feat(API): Add route for schema static files (#12770)

This commit is contained in:
Elias Meire 2025-01-28 09:53:04 +01:00 committed by GitHub
parent 1d33b9f4a7
commit d981b5659a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 203 additions and 6 deletions

View file

@ -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",

View file

@ -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');
});
});
});

View file

@ -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];

View file

@ -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);

View file

@ -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 });
}),
),
);

View file

@ -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"
},

View file

@ -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": ""
}

View file

@ -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",