diff --git a/packages/@n8n/nodes-langchain/package.json b/packages/@n8n/nodes-langchain/package.json index 4b741c173b480..38f46c858d9ef 100644 --- a/packages/@n8n/nodes-langchain/package.json +++ b/packages/@n8n/nodes-langchain/package.json @@ -7,7 +7,7 @@ "clean": "rimraf dist .turbo", "dev": "pnpm run watch", "typecheck": "tsc --noEmit", - "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && pnpm n8n-copy-icons && pnpm n8n-generate-metadata", + "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && pnpm n8n-copy-static-files && pnpm n8n-generate-metadata", "format": "biome format --write .", "format:check": "biome ci .", "lint": "eslint nodes credentials utils --quiet", diff --git a/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts b/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts index 75aa6023019c6..c5d88b1b25a51 100644 --- a/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts +++ b/packages/cli/src/__tests__/load-nodes-and-credentials.test.ts @@ -211,4 +211,44 @@ describe('LoadNodesAndCredentials', () => { expect(result.description.displayName).toBe('Special @#$% Node Tool'); }); }); + + describe('resolveSchema', () => { + let instance: LoadNodesAndCredentials; + + beforeEach(() => { + instance = new LoadNodesAndCredentials(mock(), mock(), mock(), mock()); + instance.knownNodes['n8n-nodes-base.test'] = { + className: 'Test', + sourcePath: '/nodes-base/dist/nodes/Test/Test.node.js', + }; + }); + + it('should return undefined if the node is not known', () => { + const result = instance.resolveSchema({ + node: 'n8n-nodes-base.doesNotExist', + version: '1.0.0', + resource: 'account', + operation: 'get', + }); + expect(result).toBeUndefined(); + }); + + it('should return the correct path if the node is known', () => { + const result = instance.resolveSchema({ + node: 'n8n-nodes-base.test', + version: '1.0.0', + resource: 'account', + operation: 'get', + }); + expect(result).toEqual('/nodes-base/dist/nodes/Test/__schema__/v1.0.0/account/get.json'); + }); + + it('should return the correct path if there is no resource or operation', () => { + const result = instance.resolveSchema({ + node: 'n8n-nodes-base.test', + version: '1.0.0', + }); + expect(result).toEqual('/nodes-base/dist/nodes/Test/__schema__/v1.0.0.json'); + }); + }); }); diff --git a/packages/cli/src/load-nodes-and-credentials.ts b/packages/cli/src/load-nodes-and-credentials.ts index 88cbfc05cbab2..4d6493d7ee7bc 100644 --- a/packages/cli/src/load-nodes-and-credentials.ts +++ b/packages/cli/src/load-nodes-and-credentials.ts @@ -174,6 +174,29 @@ export class LoadNodesAndCredentials { return isContainedWithin(loader.directory, filePath) ? filePath : undefined; } + resolveSchema({ + node, + version, + resource, + operation, + }: { + node: string; + version: string; + resource?: string; + operation?: string; + }): string | undefined { + const nodePath = this.known.nodes[node]?.sourcePath; + if (!nodePath) { + return undefined; + } + + const nodeParentPath = path.dirname(nodePath); + const schemaPath = ['__schema__', `v${version}`, resource, operation].filter(Boolean).join('/'); + const filePath = path.resolve(nodeParentPath, schemaPath + '.json'); + + return isContainedWithin(nodeParentPath, filePath) ? filePath : undefined; + } + getCustomDirectories(): string[] { const customDirectories = [this.instanceSettings.customExtensionDir]; diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 17ffd66ad9ca3..27955a1d90910 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -322,8 +322,27 @@ export class Server extends AbstractServer { res.sendStatus(404); }; + const serveSchemas: express.RequestHandler = async (req, res) => { + const { node, version, resource, operation } = req.params; + const filePath = this.loadNodesAndCredentials.resolveSchema({ + node, + resource, + operation, + version, + }); + + if (filePath) { + try { + await fsAccess(filePath); + return res.sendFile(filePath, cacheOptions); + } catch {} + } + res.sendStatus(404); + }; + this.app.use('/icons/@:scope/:packageName/*/*.(svg|png)', serveIcons); this.app.use('/icons/:packageName/*/*.(svg|png)', serveIcons); + this.app.use('/schemas/:node/:version/:resource?/:operation?.json', serveSchemas); const isTLSEnabled = this.globalConfig.protocol === 'https' && !!(this.sslKey && this.sslCert); diff --git a/packages/core/bin/copy-icons b/packages/core/bin/copy-static-files similarity index 56% rename from packages/core/bin/copy-icons rename to packages/core/bin/copy-static-files index bdcb011c25c4c..a178a40816f89 100755 --- a/packages/core/bin/copy-icons +++ b/packages/core/bin/copy-static-files @@ -6,13 +6,18 @@ const { cp } = require('fs/promises'); const { packageDir } = require('./common'); const limiter = pLimit(20); -const icons = glob.sync('{nodes,credentials}/**/*.{png,svg}', { cwd: packageDir }); +const staticFiles = glob.sync( + ['{nodes,credentials}/**/*.{png,svg}', 'nodes/**/__schema__/**/*.json'], + { + cwd: packageDir, + }, +); (async () => { await Promise.all( - icons.map((icon) => + staticFiles.map((path) => limiter(() => { - return cp(icon, `dist/${icon}`, { recursive: true }); + return cp(path, `dist/${path}`, { recursive: true }); }), ), ); diff --git a/packages/core/package.json b/packages/core/package.json index a4a0e651a3c84..09dfe6e27295b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -5,7 +5,7 @@ "main": "dist/index", "types": "dist/index.d.ts", "bin": { - "n8n-copy-icons": "./bin/copy-icons", + "n8n-copy-static-files": "./bin/copy-static-files", "n8n-generate-translations": "./bin/generate-translations", "n8n-generate-metadata": "./bin/generate-metadata" }, diff --git a/packages/nodes-base/nodes/ActiveCampaign/__schema__/v1.0.0/account/get.json b/packages/nodes-base/nodes/ActiveCampaign/__schema__/v1.0.0/account/get.json new file mode 100644 index 0000000000000..2e19e5948644b --- /dev/null +++ b/packages/nodes-base/nodes/ActiveCampaign/__schema__/v1.0.0/account/get.json @@ -0,0 +1,110 @@ +{ + "type": "object", + "value": [ + { + "key": "account", + "type": "Object", + "value": [ + { + "key": "accountUrl", + "type": "string", + "value": "", + "path": ".account.accountUrl" + }, + { + "key": "createdTimestamp", + "type": "string", + "value": "", + "path": ".account.createdTimestamp" + }, + { + "key": "id", + "type": "string", + "value": "", + "path": ".account.id" + }, + { + "key": "links", + "type": "Object", + "value": [ + { + "key": "accountContacts", + "type": "string", + "value": "", + "path": ".account.links.accountContacts" + }, + { + "key": "accountCustomFieldData", + "type": "string", + "value": "", + "path": ".account.links.accountCustomFieldData" + }, + { + "key": "contactEmails", + "type": "string", + "value": "", + "path": ".account.links.contactEmails" + }, + { + "key": "emailActivities", + "type": "string", + "value": "", + "path": ".account.links.emailActivities" + }, + { + "key": "notes", + "type": "string", + "value": "", + "path": ".account.links.notes" + }, + { + "key": "owner", + "type": "string", + "value": "", + "path": ".account.links.owner" + }, + { + "key": "required", + "type": "string", + "value": "", + "path": ".account.links.required" + } + ], + "path": ".account.links" + }, + { + "key": "name", + "type": "string", + "value": "", + "path": ".account.name" + }, + { + "key": "owner", + "type": "string", + "value": "", + "path": ".account.owner" + }, + { + "key": "updatedTimestamp", + "type": "string", + "value": "", + "path": ".account.updatedTimestamp" + }, + { + "key": "required", + "type": "string", + "value": "", + "path": ".account.required" + } + ], + "path": ".account" + }, + { + "key": "required", + "type": "string", + "value": "", + "path": ".required" + } + ], + "path": "" +} diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 309d93c310eda..a8ebeefd4d621 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -7,7 +7,7 @@ "clean": "rimraf dist .turbo", "dev": "pnpm watch", "typecheck": "tsc --noEmit", - "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && pnpm n8n-copy-icons && pnpm n8n-generate-translations && pnpm n8n-generate-metadata", + "build": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && pnpm n8n-copy-static-files && pnpm n8n-generate-translations && pnpm n8n-generate-metadata", "format": "biome format --write .", "format:check": "biome ci .", "lint": "eslint nodes credentials utils test --quiet && node ./scripts/validate-load-options-methods.js",