Compare commits

...

5 Commits

Author SHA1 Message Date
Corentin Thomasset
f5d951cc82 chore(n8n): added scope in package name 2025-08-04 20:35:35 +02:00
Corentin Thomasset
47f9c5b186 refactor(n8n): updated changeset 2025-08-04 13:54:15 +02:00
Corentin Thomasset
0b97e58785 chore(n8n): added workflow file for n8n nodes 2025-08-04 13:50:43 +02:00
Corentin Thomasset
d51779aeb8 refactor(n8n): auto lint 2025-08-04 13:25:33 +02:00
Marco Mihai Condrache
8f30ec0281 feat(n8n): initial setup of n8n node package (#443)
* feat: n8n package implementation

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: typo

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: wrong requests

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: pagination

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: search

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* feat: use correct regex

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: general fixes

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: use color type

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: specs

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: result

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: file download

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* chore: changeset

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* feat: add readme

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

* fix: typo

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>

---------

Signed-off-by: Marco Mihai Condrache <52580954+marcocondrache@users.noreply.github.com>
2025-08-04 13:23:42 +02:00
40 changed files with 3509 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
"n8n-nodes-papra": major
---
Added n8n nodes package for Papra

View File

@@ -0,0 +1,41 @@
name: CI - N8N Nodes
on:
pull_request:
push:
branches:
- main
jobs:
ci-packages-n8n-nodes:
name: CI - N8N Nodes
runs-on: ubuntu-latest
defaults:
run:
working-directory: packages/n8n-nodes
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm i
- name: Run linters
run: pnpm lint
- name: Type check
run: pnpm typecheck
# - name: Run unit test
# run: pnpm test
- name: Build the app
run: pnpm build

View File

@@ -0,0 +1,74 @@
# n8n Integration
A community node package that integrates [Papra](https://papra.app) (the document archiving platform) with [n8n](https://n8n.io), enabling you to automate document management workflows.
## Installation
1. In your n8n instance, go to **Settings****Community Nodes**
2. Click **Install** and enter: `@papra/n8n-nodes-papra`
3. Install the package and restart your n8n instance
## Setup
### 1. Create API Credentials
Before using this integration, you need to create API credentials in your Papra workspace:
1. Log in to your Papra instance
2. Navigate to **Settings****API Keys**
3. Click **Create New API Key**
4. Copy the generated API key and your Organization ID (from the url)
For detailed instructions, visit the [Papra API documentation](https://docs.papra.app/resources/api-endpoints/#authentication).
### 2. Configure n8n Credentials
1. In n8n, create a new workflow
2. Add a Papra node
3. Create new credentials with:
- **Papra API URL**: `https://api.papra.app` (or your self-hosted instance URL)
- **Organization ID**: Your organization ID from Papra
- **API Key**: Your generated API key
## Available Operations
| Resource | Operations |
|----------|------------|
| Document | `create`, `list`, `get`, `update`, `remove`, `get_file`, `get_activity` |
| Tag | Standard CRUD operations |
| Document Tag | Link/unlink tags to/from documents |
| Statistics | Retrieve workspace analytics |
| Trash | List deleted documents |
## Development
### Prerequisites
- Node.js 20.15 or higher
- pnpm package manager
- n8n instance for testing
- you can use `pnpx n8n` or `pnpm i -g n8n` command to install n8n globally
### Testing the Integration
#### Option 1: Local n8n Instance
1. Build this package:
```bash
pnpm run build
```
2. Link the package to your local n8n:
```bash
# Navigate to your n8n nodes directory
cd ~/.n8n/nodes
# Install the package locally
npm install /path/to/papra/packages/n8n-nodes
```
3. Start n8n:
```bash
npx n8n
```
4. In n8n, create a new workflow and search for "Papra" to find the node
#### Option 2: Docker
Build a custom n8n Docker image with the Papra node included. Follow the [n8n documentation](https://docs.n8n.io/integrations/creating-nodes/deploy/install-private-nodes/#install-your-node-in-a-docker-n8n-instance) for detailed instructions.

View File

@@ -0,0 +1,41 @@
import type { IAuthenticateGeneric, ICredentialType, INodeProperties } from 'n8n-workflow';
export class PapraApi implements ICredentialType {
name = 'papraApi';
displayName = 'Papra API';
documentationUrl = 'https://docs.papra.app/resources/api-endpoints/#authentication';
properties: INodeProperties[] = [
{
name: 'url',
displayName: 'Papra API URL',
default: 'https://api.papra.app',
required: true,
type: 'string',
validateType: 'url',
},
{
name: 'organization_id',
displayName: 'Organization ID',
default: '',
required: true,
type: 'string',
},
{
name: 'apiKey',
displayName: 'Papra API Key',
default: '',
required: true,
type: 'string',
typeOptions: { password: true },
},
];
authenticate: IAuthenticateGeneric = {
type: 'generic',
properties: {
headers: {
Authorization: '=Bearer {{$credentials.apiKey}}',
},
},
};
}

View File

@@ -0,0 +1,24 @@
import antfu from '@antfu/eslint-config';
export default antfu({
stylistic: {
semi: true,
},
// TODO: include the n8n rules package when it's eslint-9 ready
// https://github.com/ivov/eslint-plugin-n8n-nodes-base/issues/196
rules: {
// To allow export on top of files
'ts/no-use-before-define': ['error', { allowNamedExports: true, functions: false }],
'curly': ['error', 'all'],
'vitest/consistent-test-it': ['error', { fn: 'test' }],
'ts/consistent-type-definitions': ['error', 'type'],
'style/brace-style': ['error', '1tbs', { allowSingleLine: false }],
'unused-imports/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
}],
},
});

View File

@@ -0,0 +1,16 @@
const path = require('node:path');
const { task, src, dest } = require('gulp');
task('build:icons', copyIcons);
function copyIcons() {
const nodeSource = path.resolve('nodes', '**', '*.{png,svg}');
const nodeDestination = path.resolve('dist', 'nodes');
src(nodeSource).pipe(dest(nodeDestination));
const credSource = path.resolve('credentials', '**', '*.{png,svg}');
const credDestination = path.resolve('dist', 'credentials');
return src(credSource).pipe(dest(credDestination));
}

View File

View File

@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.papra",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.papra.app/resources/api-endpoints/#authentication"
}
],
"primaryDocumentation": [
{
"url": "https://docs.papra.app/"
}
]
}
}

View File

@@ -0,0 +1,24 @@
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
import { VersionedNodeType } from 'n8n-workflow';
import { PapraV1 } from './v1/PapraV1.node';
export class Papra extends VersionedNodeType {
constructor() {
const baseDescription: INodeTypeBaseDescription = {
displayName: 'Papra',
name: 'papra',
icon: 'file:papra.svg',
group: ['input'],
description: 'Read, update, write and delete data from Papra',
defaultVersion: 1,
usableAsTool: true,
};
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
1: new PapraV1(baseDescription),
};
super(nodeVersions, baseDescription);
}
}

View File

@@ -0,0 +1,31 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { router } from './actions/router';
import * as version from './actions/version';
import { listSearch } from './methods';
export class PapraV1 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...version.description,
usableAsTool: true,
};
}
methods = {
listSearch,
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
return await router.call(this);
}
}

View File

@@ -0,0 +1,83 @@
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { Buffer } from 'node:buffer';
import FormData from 'form-data';
import { apiRequest } from '../../transport/index.js';
export const description: INodeProperties[] = [
{
displayName: 'Input Binary Field',
name: 'binary_property_name',
default: 'data',
displayOptions: {
show: {
resource: ['document'],
operation: ['create'],
},
},
hint: 'The name of the input field containing the file data to be processed',
required: true,
type: 'string',
},
{
displayName: 'Additional Fields',
name: 'additional_fields',
type: 'collection',
default: {},
displayOptions: {
show: {
resource: ['document'],
operation: ['create'],
},
},
placeholder: 'Add Field',
options: [
{
displayName: 'OCR languages',
name: 'ocr_languages',
default: '',
description: 'The languages of the document',
type: 'string',
},
],
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const endpoint = `/documents`;
const formData = new FormData();
const binaryPropertyName = this.getNodeParameter('binary_property_name', itemIndex) as string;
const binaryData = this.helpers.assertBinaryData(itemIndex, binaryPropertyName);
const data = binaryData.id
? await this.helpers.getBinaryStream(binaryData.id)
: Buffer.from(binaryData.data, 'base64');
formData.append('file', data, {
filename: binaryData.fileName,
contentType: binaryData.mimeType,
});
const additionalFields = this.getNodeParameter('additional_fields', itemIndex) as any;
Object.entries({
ocrLanguages: additionalFields.ocr_languages,
})
.filter(([, value]) => value !== undefined && value !== '')
.forEach(([key, value]) => {
formData.append(key, value);
});
const response = (await apiRequest.call(
this,
itemIndex,
'POST',
endpoint,
undefined,
undefined,
{ headers: formData.getHeaders(), formData },
)) as any;
return { json: { results: [response] } };
}

View File

@@ -0,0 +1,77 @@
import type { INodeProperties } from 'n8n-workflow';
import * as create from './create.operation';
import * as get from './get.operation';
import * as get_activity from './get_activity.operation';
import * as get_file from './get_file.operation';
import * as list from './list.operation';
import * as remove from './remove.operation';
import * as update from './update.operation';
export {
create,
get,
get_activity,
get_file,
list,
remove,
update,
};
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
default: 'list',
displayOptions: {
show: { resource: ['document'] },
},
noDataExpression: true,
options: [
{
name: 'Create a document',
value: 'create',
action: 'Create a new document',
},
{
name: 'List documents',
value: 'list',
action: 'List all documents',
},
{
name: 'Update a document',
value: 'update',
action: 'Update a document',
},
{
name: 'Get a document',
value: 'get',
action: 'Get a document',
},
{
name: 'Get the document file',
value: 'get_file',
action: 'Get the file of the document',
},
{
name: 'Delete a document',
value: 'remove',
action: 'Delete a document',
},
{
name: 'Get the document activity log',
value: 'get_activity',
action: 'Get the activity log of a document',
},
],
type: 'options',
},
...create.description,
...list.description,
...update.description,
...get.description,
...get_file.description,
...get_activity.description,
...remove.description,
];

View File

@@ -0,0 +1,83 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['document'],
operation: ['get'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Document...`,
type: 'list',
typeOptions: {
searchListMethod: 'documentSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Document ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9_]+$',
errorMessage: 'The ID must be valid',
},
},
],
},
{
displayName: 'By URL',
name: 'url',
placeholder: `Enter Document URL...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
errorMessage: 'The URL must be a valid Papra document URL (e.g. https://papra.example.com/organizations/org_xxx/documents/doc_xxx?tab=info)',
},
},
],
extractValue: {
type: 'regex',
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
},
},
],
placeholder: 'ID of the document',
required: true,
type: 'resourceLocator',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const id = (this.getNodeParameter('id', itemIndex) as INodeParameterResourceLocator).value;
const endpoint = `/documents/${id}`;
const response = (await apiRequest.call(this, itemIndex, 'GET', endpoint)) as any;
return { json: { results: [response] } };
}

View File

@@ -0,0 +1,100 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import {
NodeOperationError,
} from 'n8n-workflow';
import { apiRequestPaginated } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['document'],
operation: ['get_activity'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Document...`,
type: 'list',
typeOptions: {
searchListMethod: 'documentSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Document ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9_]+$',
errorMessage: 'The ID must be valid',
},
},
],
},
{
displayName: 'By URL',
name: 'url',
placeholder: `Enter Document URL...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
errorMessage: 'The URL must be a valid Papra document URL (e.g. https://papra.example.com/organizations/org_xxx/documents/doc_xxx?tab=info)',
},
},
],
extractValue: {
type: 'regex',
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
},
},
],
placeholder: 'ID of the document',
required: true,
type: 'resourceLocator',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const id = (this.getNodeParameter('id', itemIndex) as INodeParameterResourceLocator).value;
const endpoint = `/documents/${id}/activity`;
const responses = (await apiRequestPaginated.call(this, itemIndex, 'GET', endpoint)) as any[];
const statusCode = responses.reduce((acc, response) => acc + response.statusCode, 0) / responses.length;
if (statusCode !== 200) {
throw new NodeOperationError(
this.getNode(),
`The documents you are requesting could not be found`,
{
description: JSON.stringify(
responses.map(response => response?.body?.details ?? response?.statusMessage),
),
},
);
}
return {
json: { results: responses.flatMap(response => response.body.activities) },
};
}

View File

@@ -0,0 +1,111 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { Buffer } from 'node:buffer';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['document'],
operation: ['get_file'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Document...`,
type: 'list',
typeOptions: {
searchListMethod: 'documentSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Document ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9_]+$',
errorMessage: 'The ID must be valid',
},
},
],
},
{
displayName: 'By URL',
name: 'url',
placeholder: `Enter Document URL...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
errorMessage: 'The URL must be a valid Papra document URL (e.g. https://papra.example.com/organizations/org_xxx/documents/doc_xxx?tab=info)',
},
},
],
extractValue: {
type: 'regex',
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
},
},
],
placeholder: 'ID of the document',
required: true,
type: 'resourceLocator',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const id = (this.getNodeParameter('id', itemIndex) as INodeParameterResourceLocator).value;
const endpoint = `/documents/${id}`;
const preview = (await apiRequest.call(
this,
itemIndex,
'GET',
`${endpoint}/file`,
undefined,
undefined,
{
json: false,
encoding: null,
resolveWithFullResponse: true,
},
)) as any;
// TODO: fix
const filename = preview.headers['content-disposition']
?.match(/filename="(?:b['"])?([^"]+)['"]?"/)?.[1]
?.replace(/^['"]|['"]$/g, '') ?? `${id}.pdf`;
const mimeType = preview.headers['content-type'];
return {
json: {},
binary: {
data: await this.helpers.prepareBinaryData(
Buffer.from(preview.body),
filename,
mimeType,
),
},
};
}

View File

@@ -0,0 +1,37 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import {
NodeOperationError,
} from 'n8n-workflow';
import { apiRequestPaginated } from '../../transport';
export const description: INodeProperties[] = [];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const endpoint = '/documents';
const responses = (await apiRequestPaginated.call(this, itemIndex, 'GET', endpoint)) as any[];
const statusCode = responses.reduce((acc, response) => acc + response.statusCode, 0) / responses.length;
if (statusCode !== 200) {
throw new NodeOperationError(
this.getNode(),
`The documents you are requesting could not be found`,
{
description: JSON.stringify(
responses.map(response => response?.body?.error?.message ?? response?.error?.code),
),
},
);
}
return {
json: { results: responses.flatMap(response => response.body.documents) },
};
}

View File

@@ -0,0 +1,82 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['document'],
operation: ['remove'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Document...`,
type: 'list',
typeOptions: {
searchListMethod: 'documentSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Document ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9_]+$',
errorMessage: 'The ID must be valid',
},
},
],
},
{
displayName: 'By URL',
name: 'url',
placeholder: `Enter Document URL...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
errorMessage: 'The URL must be a valid Papra document URL (e.g. https://papra.example.com/organizations/org_xxx/documents/doc_xxx?tab=info)',
},
},
],
extractValue: {
type: 'regex',
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
},
},
],
placeholder: 'ID of the document',
required: true,
type: 'resourceLocator',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const id = (this.getNodeParameter('id', itemIndex) as INodeParameterResourceLocator).value;
const endpoint = `/documents/${id}`;
await apiRequest.call(this, itemIndex, 'DELETE', endpoint);
return { json: { results: [true] } };
}

View File

@@ -0,0 +1,130 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
default: { mode: 'list', value: '' },
description: 'ID of the document',
displayOptions: {
show: {
resource: ['document'],
operation: ['update'],
},
},
hint: 'The ID of the document',
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Document...`,
type: 'list',
typeOptions: {
searchListMethod: 'documentSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Document ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9_]+$',
errorMessage: 'The ID must be valid',
},
},
],
},
{
displayName: 'By URL',
name: 'url',
placeholder: `Enter Document URL...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
errorMessage: 'The URL must be a valid Papra document URL (e.g. https://papra.example.com/organizations/org_xxx/documents/doc_xxx?tab=info)',
},
},
],
extractValue: {
type: 'regex',
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
},
},
],
placeholder: 'ID of the document',
required: true,
type: 'resourceLocator',
},
{
displayName: 'Update fields',
name: 'update_fields',
default: {},
displayOptions: {
show: {
resource: ['document'],
operation: ['update'],
},
},
options: [
{
displayName: 'Name',
name: 'name',
default: '',
description: 'The name of the document',
type: 'string',
},
{
displayName: 'Content',
name: 'content',
default: '',
description: 'The content of the document, for search purposes',
type: 'string',
},
],
placeholder: 'Add Field',
type: 'collection',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const id = (this.getNodeParameter('id', itemIndex) as INodeParameterResourceLocator).value;
const endpoint = `/documents/${id}`;
const updateFields = this.getNodeParameter('update_fields', itemIndex, {}) as any;
const body: { [key: string]: any } = {};
for (const key of Object.keys(updateFields)) {
if (updateFields[key] !== null && updateFields[key] !== undefined) {
body[key] = updateFields[key];
}
}
const response = (await apiRequest.call(
this,
itemIndex,
'PATCH',
endpoint,
body,
)) as any;
return { json: { results: [response] } };
}

View File

@@ -0,0 +1,127 @@
import type { IExecuteFunctions, INodeExecutionData, INodeParameterResourceLocator, INodeProperties } from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['document_tag'],
operation: ['create'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Document...`,
type: 'list',
typeOptions: {
searchListMethod: 'documentSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Document ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9_]+$',
errorMessage: 'The ID must be valid',
},
},
],
},
{
displayName: 'By URL',
name: 'url',
placeholder: `Enter Document URL...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
errorMessage: 'The URL must be a valid Papra document URL (e.g. https://papra.example.com/organizations/org_xxx/documents/doc_xxx?tab=info)',
},
},
],
extractValue: {
type: 'regex',
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
},
},
],
placeholder: 'ID of the document',
required: true,
type: 'resourceLocator',
},
{
displayName: 'Tag ID',
name: 'tag_id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['document_tag'],
operation: ['create'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Tag...`,
type: 'list',
typeOptions: {
searchListMethod: 'tagSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Tag ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9]+$',
errorMessage: 'The ID must be an alphanumeric string',
},
},
],
},
],
placeholder: 'ID of the tag',
required: true,
type: 'resourceLocator',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const document_id = (this.getNodeParameter('id', itemIndex) as INodeParameterResourceLocator).value;
const tag_id = (this.getNodeParameter('tag_id', itemIndex) as INodeParameterResourceLocator).value;
const endpoint = `/documents/${document_id}/tags`;
const body = {
tagId: tag_id,
};
await apiRequest.call(this, itemIndex, 'POST', endpoint, body);
return {
json: { results: [true] },
};
}

View File

@@ -0,0 +1,33 @@
import type { INodeProperties } from 'n8n-workflow';
import * as create from './create.operation';
import * as remove from './remove.operation';
export { create, remove };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
default: 'list',
displayOptions: {
show: { resource: ['document_tag'] },
},
noDataExpression: true,
options: [
{
name: 'Add a tag to a document',
value: 'create',
action: 'Add a tag to a document',
},
{
name: 'Remove a tag from a document',
value: 'remove',
action: 'Remove a tag from a document',
},
],
type: 'options',
},
...create.description,
...remove.description,
];

View File

@@ -0,0 +1,126 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['document_tag'],
operation: ['remove'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Document...`,
type: 'list',
typeOptions: {
searchListMethod: 'documentSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Document ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9_]+$',
errorMessage: 'The ID must be valid',
},
},
],
},
{
displayName: 'By URL',
name: 'url',
placeholder: `Enter Document URL...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
errorMessage: 'The URL must be a valid Papra document URL (e.g. https://papra.example.com/organizations/org_xxx/documents/doc_xxx?tab=info)',
},
},
],
extractValue: {
type: 'regex',
regex: '^(?:http|https)://(?:.+?)/documents/([a-zA-Z0-9_]+)/?(?:\\?.*)?$',
},
},
],
placeholder: 'ID of the document',
required: true,
type: 'resourceLocator',
},
{
displayName: 'Tag ID',
name: 'tag_id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['document_tag'],
operation: ['remove'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Tag...`,
type: 'list',
typeOptions: {
searchListMethod: 'tagSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Tag ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9]+$',
errorMessage: 'The ID must be an alphanumeric string',
},
},
],
},
],
placeholder: 'ID of the tag',
required: true,
type: 'resourceLocator',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const document_id = (this.getNodeParameter('id', itemIndex) as INodeParameterResourceLocator).value;
const tag_id = (this.getNodeParameter('tag_id', itemIndex) as INodeParameterResourceLocator).value;
const endpoint = `/documents/${document_id}/tags/${tag_id}`;
await apiRequest.call(this, itemIndex, 'DELETE', endpoint);
return { json: { results: [true] } };
}

View File

@@ -0,0 +1,9 @@
import type { AllEntities } from 'n8n-workflow';
export type PapraType = AllEntities<{
statistics: 'get';
document: 'create' | 'list' | 'update' | 'get' | 'get_file' | 'get_activity' | 'remove';
document_tag: 'create' | 'remove';
tag: 'create' | 'list' | 'update' | 'remove';
trash: 'list';
}>;

View File

@@ -0,0 +1,54 @@
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import type { PapraType } from './node.type.ts';
import * as document from './document/document.resource';
import * as document_tag from './document_tag/document_tag.resource';
import * as statistics from './statistics/statistics.resource';
import * as tag from './tag/tag.resource';
import * as trash from './trash/trash.resource';
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const returnData: INodeExecutionData[] = [];
for (let itemIndex = 0; itemIndex < this.getInputData().length; itemIndex++) {
const resource = this.getNodeParameter<PapraType>('resource', itemIndex);
const operation = this.getNodeParameter('operation', itemIndex);
const papraNodeData = { resource, operation } as PapraType;
try {
switch (papraNodeData.resource) {
case 'statistics':
returnData.push(await statistics[papraNodeData.operation].execute.call(this, itemIndex));
break;
case 'document':
returnData.push(
await document[papraNodeData.operation].execute.call(this, itemIndex),
);
break;
case 'document_tag':
returnData.push(
await document_tag[papraNodeData.operation].execute.call(this, itemIndex),
);
break;
case 'tag':
returnData.push(
await tag[papraNodeData.operation].execute.call(this, itemIndex),
);
break;
case 'trash':
returnData.push(
await trash[papraNodeData.operation].execute.call(this, itemIndex),
);
break;
}
} catch (error) {
if (error.description?.includes('cannot accept the provided value')) {
error.description += '. Consider using \'Typecast\' option';
}
throw error;
}
}
return [returnData];
}

View File

@@ -0,0 +1,20 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const endpoint = `/documents/statistics`;
const response = (await apiRequest.call(this, itemIndex, 'GET', endpoint)) as any;
return {
json: response,
};
}

View File

@@ -0,0 +1,26 @@
import type { INodeProperties } from 'n8n-workflow';
import * as get from './get.operation';
export { get };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
default: 'get',
displayOptions: {
show: { resource: ['statistics'] },
},
noDataExpression: true,
options: [
{
name: 'Get statistics of the organization',
value: 'get',
action: 'Get statistics',
},
],
type: 'options',
},
...get.description,
];

View File

@@ -0,0 +1,60 @@
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'Name',
name: 'name',
displayOptions: {
show: {
resource: ['tag'],
operation: ['create'],
},
},
placeholder: 'Name of the tag',
required: true,
type: 'string',
default: '',
},
{
displayName: 'Color',
name: 'color',
default: '#000000',
displayOptions: {
show: {
resource: ['tag'],
operation: ['create'],
},
},
type: 'color',
},
{
displayName: 'Description',
name: 'description',
default: '',
displayOptions: {
show: {
resource: ['tag'],
operation: ['create'],
},
},
placeholder: 'Description of the tag',
type: 'string',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const endpoint = `/tags`;
const body = {
name: this.getNodeParameter('name', itemIndex),
color: this.getNodeParameter('color', itemIndex)?.toString().toLowerCase(),
description: this.getNodeParameter('description', itemIndex, ''),
};
const response = (await apiRequest.call(this, itemIndex, 'POST', endpoint, body)) as any;
return { json: { results: [response] } };
}

View File

@@ -0,0 +1,20 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const endpoint = '/tags';
const response = (await apiRequest.call(this, itemIndex, 'GET', endpoint)) as any;
return {
json: { results: response.tags },
};
}

View File

@@ -0,0 +1,63 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['tag'],
operation: ['remove'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Tag...`,
type: 'list',
typeOptions: {
searchListMethod: 'tagSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Tag ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9_]+$',
errorMessage: 'The ID must be valid',
},
},
],
},
],
placeholder: 'ID of the tag',
required: true,
type: 'resourceLocator',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const id = (this.getNodeParameter('id', itemIndex) as INodeParameterResourceLocator).value;
const endpoint = `/tags/${id}`;
await apiRequest.call(this, itemIndex, 'DELETE', endpoint);
return { json: { results: [true] } };
}

View File

@@ -0,0 +1,47 @@
import type { INodeProperties } from 'n8n-workflow';
import * as create from './create.operation';
import * as list from './list.operation';
import * as remove from './remove.operation';
import * as update from './update.operation';
export { create, list, remove, update };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
default: 'list',
displayOptions: {
show: { resource: ['tag'] },
},
noDataExpression: true,
options: [
{
name: 'Create a tag',
value: 'create',
action: 'Create a new tag',
},
{
name: 'Delete a tag',
value: 'remove',
action: 'Delete a tag',
},
{
name: 'List tags',
value: 'list',
action: 'List all tags',
},
{
name: 'Update a tag',
value: 'update',
action: 'Update a tag',
},
],
type: 'options',
},
...create.description,
...list.description,
...remove.description,
...update.description,
];

View File

@@ -0,0 +1,111 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'ID',
name: 'id',
default: { mode: 'list', value: '' },
displayOptions: {
show: {
resource: ['tag'],
operation: ['update'],
},
},
modes: [
{
displayName: 'From List',
name: 'list',
placeholder: `Select a Tag...`,
type: 'list',
typeOptions: {
searchListMethod: 'tagSearch',
searchFilterRequired: false,
searchable: true,
},
},
{
displayName: 'By ID',
name: 'id',
placeholder: `Enter Tag ID...`,
type: 'string',
validation: [
{
type: 'regex',
properties: {
regex: '^[a-zA-Z0-9_]+$',
errorMessage: 'The ID must be valid',
},
},
],
},
],
placeholder: 'ID of the tag',
required: true,
type: 'resourceLocator',
},
{
displayName: 'Update fields',
name: 'update_fields',
default: {},
displayOptions: {
show: {
resource: ['tag'],
operation: ['update'],
},
},
options: [
{
displayName: 'Name',
name: 'name',
placeholder: 'Name of the tag',
type: 'string',
default: '',
},
{
displayName: 'Color',
name: 'color',
default: '#000000',
type: 'color',
},
{
displayName: 'Description',
name: 'description',
default: '',
placeholder: 'Description of the tag',
type: 'string',
},
],
placeholder: 'Add Field',
type: 'collection',
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const id = (this.getNodeParameter('id', itemIndex) as INodeParameterResourceLocator).value;
const endpoint = `/tags/${id}`;
const updateFields = this.getNodeParameter('update_fields', itemIndex, {}) as {
[key: string]: any;
};
const body: { [key: string]: any } = {};
for (const key of Object.keys(updateFields)) {
if (updateFields[key] !== null && updateFields[key] !== undefined) {
body[key] = key === 'color' ? updateFields[key].toString().toLowerCase() : updateFields[key];
}
}
const response = (await apiRequest.call(this, itemIndex, 'PUT', endpoint, body)) as any;
return { json: { results: [response] } };
}

View File

@@ -0,0 +1,37 @@
import type {
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import {
NodeOperationError,
} from 'n8n-workflow';
import { apiRequestPaginated } from '../../transport';
export const description: INodeProperties[] = [];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const endpoint = `/documents/deleted`;
const responses = (await apiRequestPaginated.call(this, itemIndex, 'GET', endpoint)) as any[];
const statusCode = responses.reduce((acc, response) => acc + response.statusCode, 0) / responses.length;
if (statusCode !== 200) {
throw new NodeOperationError(
this.getNode(),
`The trash you are requesting could not be found`,
{
description: JSON.stringify(
responses.map(response => response?.body?.details ?? response?.statusMessage),
),
},
);
}
return {
json: { results: responses.flatMap(response => response.body.documents) },
};
}

View File

@@ -0,0 +1,26 @@
import type { INodeProperties } from 'n8n-workflow';
import * as list from './list.operation';
export { list };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
default: 'list',
displayOptions: {
show: { resource: ['trash'] },
},
noDataExpression: true,
options: [
{
name: 'List trash',
value: 'list',
action: 'List all trash',
},
],
type: 'options',
},
...list.description,
];

View File

@@ -0,0 +1,66 @@
import type { INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import * as document from './document/document.resource';
import * as document_tag from './document_tag/document_tag.resource';
import * as statistics from './statistics/statistics.resource';
import * as tag from './tag/tag.resource';
import * as trash from './trash/trash.resource';
export const description: INodeTypeDescription = {
displayName: 'Papra',
name: 'papra',
icon: 'file:papra.svg',
group: ['input'],
version: 1,
subtitle: '={{ $parameter.operation + ": " + $parameter.resource }}',
description: 'Consume documents and metadata from Papra API',
defaults: { name: 'Papra' },
credentials: [{ name: 'papraApi', required: true }],
inputs: [NodeConnectionType.Main],
outputs: [NodeConnectionType.Main],
properties: [
{
displayName: 'Resource',
name: 'resource',
default: 'document',
noDataExpression: true,
options: [
{
name: 'Statistics',
value: 'statistics',
description: 'Statistics about the documents',
},
{
name: 'Document',
value: 'document',
description: 'Scanned document or file saved in Papra',
},
{
name: 'Document tag',
value: 'document_tag',
description: 'Associate a tag to a document',
},
{
name: 'Tag',
value: 'tag',
description: 'Label for documents',
},
{
name: 'Trash',
value: 'trash',
description: 'All deleted documents',
},
],
type: 'options',
},
...document.description,
...tag.description,
...trash.description,
...statistics.description,
...document_tag.description,
],
};

View File

@@ -0,0 +1 @@
export * as listSearch from './listSearch';

View File

@@ -0,0 +1,65 @@
import type {
ILoadOptionsFunctions,
INodeListSearchResult,
} from 'n8n-workflow';
import { apiRequest, apiRequestPaginated } from '../transport';
export async function documentSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
if (filter && filter.trim().length >= 3) {
const endpoint = `/documents/search`;
const query = { searchQuery: filter };
const responses = (await apiRequestPaginated.call(
this,
0,
'GET',
endpoint,
undefined,
query,
)) as {
body: {
documents: { id: number; name: string }[];
};
}[];
const [result] = responses;
return {
results: result
? result.body.documents.map(item => ({
name: item.name,
value: item.id,
}))
: [],
};
}
const endpoint = `/documents`;
const response = (await apiRequest.call(this, 0, 'GET', endpoint, {}, { pageSize: 30, pageIndex: 0 })) as { documents: { id: number; name: string }[] };
return {
results: response.documents.map(item => ({
name: item.name,
value: item.id,
})),
};
}
export async function tagSearch(
this: ILoadOptionsFunctions,
filter?: string,
): Promise<INodeListSearchResult> {
const endpoint = `/tags`;
const response = (await apiRequest.call(this, 0, 'GET', endpoint)) as { tags: { id: number; name: string }[] };
return {
results: response.tags
.filter(item => !filter || item.name.includes(filter))
.map(item => ({
name: item.name.trim().length > 80 ? `${item.name.trim().slice(0, 80)}...` : item.name.trim(),
value: item.id,
})),
};
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#403a3a" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path d="M14 3v4a1 1 0 0 0 1 1h4"/><path d="M17 21H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7l5 5v11a2 2 0 0 1-2 2zM9 9h1m-1 4h6m-6 4h6"/></g></svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@@ -0,0 +1,96 @@
import type {
IDataObject,
IExecuteFunctions,
IHttpRequestMethods,
ILoadOptionsFunctions,
IRequestOptions,
PaginationOptions,
} from 'n8n-workflow';
export async function apiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions,
itemIndex: number,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
query?: IDataObject,
option: IRequestOptions = {},
): Promise<unknown> {
const queryParams = query || {};
const credentials = await this.getCredentials('papraApi');
const options: IRequestOptions = {
headers: {},
method,
body,
qs: queryParams,
uri: `${credentials.url}/api/organizations/${credentials.organization_id}${endpoint}`,
json: true,
};
if (Object.keys(option).length) {
Object.assign(options, option);
}
if (!Object.keys(body).length) {
options.body = undefined;
}
return this.helpers.requestWithAuthentication.call(
this,
'papraApi',
options,
undefined,
itemIndex,
);
}
export async function apiRequestPaginated(
this: IExecuteFunctions | ILoadOptionsFunctions,
itemIndex: number,
method: IHttpRequestMethods,
endpoint: string,
body: IDataObject = {},
query?: IDataObject,
option: IRequestOptions = {},
): Promise<unknown[]> {
query = query || {};
const credentials = await this.getCredentials('papraApi');
const options: IRequestOptions = {
headers: {},
method,
body,
qs: query,
uri: `${credentials.url}/api/organizations/${credentials.organization_id}${endpoint}`,
json: true,
};
if (Object.keys(option).length) {
Object.assign(options, option);
}
if (!Object.keys(body).length) {
delete options.body;
}
const paginationOptions: PaginationOptions = {
// TODO: make continue condition generic
continue: '={{ $response.body.documents && $response.body.documents.length > 0 }}',
request: {
qs: {
pageSize: '={{ $request.qs.pageSize || 50 }}',
pageIndex: '={{ $pageCount }}',
},
},
requestInterval: 100,
};
return this.helpers.requestWithAuthenticationPaginated.call(
this,
options,
itemIndex,
paginationOptions,
'papraApi',
);
}

View File

@@ -0,0 +1,61 @@
{
"name": "@papra/n8n-nodes-papra",
"version": "0.1.0",
"description": "n8n nodes for Papra, the document archiving platform.",
"author": "Corentin Thomasset <corentinth@proton.me> (https://corentin.tech)",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
"url": "https://github.com/papra-hq/papra",
"directory": "packages/n8n-nodes"
},
"bugs": {
"url": "https://github.com/papra-hq/papra/issues"
},
"keywords": [
"n8n-community-node-package",
"n8n",
"papra",
"document",
"archiving",
"self-hosted"
],
"main": "index.js",
"files": [
"dist"
],
"engines": {
"node": ">=20.15"
},
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint --fix .",
"prepublishOnly": "pnpm build",
"build": "pnpm build:clean && tsc && gulp build:icons",
"build:clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"n8n": {
"n8nNodesApiVersion": 1,
"credentials": [
"dist/credentials/PapraApi.credentials.js"
],
"nodes": [
"dist/nodes/Papra.node.js"
]
},
"peerDependencies": {
"n8n-workflow": "*"
},
"dependencies": {
"form-data": "^4.0.1"
},
"devDependencies": {
"@antfu/eslint-config": "catalog:",
"eslint": "catalog:",
"eslint-plugin-n8n-nodes-base": "^1.16.3",
"gulp": "^5.0.0",
"typescript": "catalog:",
"unbuild": "catalog:"
}
}

View File

@@ -0,0 +1,30 @@
{
"compilerOptions": {
"incremental": true,
"target": "es2019",
"lib": ["es2019", "es2020", "es2022.error"],
"module": "commonjs",
"moduleResolution": "node",
"resolveJsonModule": true,
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"useUnknownInCatchVariables": false,
"declaration": true,
"outDir": "./dist/",
"preserveConstEnums": true,
"removeComments": true,
"sourceMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": [
"credentials/**/*",
"nodes/**/*",
"nodes/**/*.json",
"package.json"
]
}

1455
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff