feat(properties): prepare property commands before execution
Build and Push Docker Container / build-and-push (push) Successful in 4m28s
Build and Push Docker Container / build-and-push (push) Successful in 4m28s
- Create missing note files before running property:set. - Default date-only datetime values to midnight. - Keep property names out of path authorization checks. - Split policy, vault, daily note, and command target helpers. - Split n8n node args, description, output, and API response helpers. - Split API tests by auth, command, daily note, and property behavior. - Bump API and n8n node versions.
This commit is contained in:
@@ -4,294 +4,14 @@ import type {
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { compactArgs, executeObsidianCommand, kv } from '../../shared/ObsidianApiClient';
|
||||
import { executeObsidianCommand } from '../../shared/ObsidianApiClient';
|
||||
import { buildArgs } from './argBuilders';
|
||||
import { nodeDescription } from './description';
|
||||
import { outputForResult } from './output';
|
||||
|
||||
export class ObsidianApi implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Obsidian API',
|
||||
name: 'obsidianApi',
|
||||
icon: 'file:obsidian.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"]}}',
|
||||
description: 'Run official Obsidian CLI commands through the Obsidian API Docker service',
|
||||
defaults: {
|
||||
name: 'Obsidian API',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'obsidianApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{ name: 'Daily Note', value: 'daily' },
|
||||
{ name: 'Note', value: 'note' },
|
||||
{ name: 'Property', value: 'property' },
|
||||
{ name: 'Raw Command', value: 'raw' },
|
||||
{ name: 'Search', value: 'search' },
|
||||
{ name: 'Vault', value: 'vault' },
|
||||
],
|
||||
default: 'note',
|
||||
},
|
||||
|
||||
// Note operations
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['note'] } },
|
||||
options: [
|
||||
{ name: 'Append', value: 'append' },
|
||||
{ name: 'Create', value: 'create' },
|
||||
{ name: 'Delete', value: 'delete' },
|
||||
{ name: 'List Files', value: 'files' },
|
||||
{ name: 'Prepend', value: 'prepend' },
|
||||
{ name: 'Read', value: 'read' },
|
||||
{ name: 'Rename', value: 'rename' },
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
{
|
||||
displayName: 'Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['note'], operation: ['append', 'create', 'delete', 'prepend', 'read', 'rename'] } },
|
||||
default: '',
|
||||
placeholder: 'Inbox/Note.md',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Content',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
typeOptions: { rows: 8 },
|
||||
displayOptions: { show: { resource: ['note'], operation: ['append', 'create', 'prepend'] } },
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Overwrite',
|
||||
name: 'overwrite',
|
||||
type: 'boolean',
|
||||
displayOptions: { show: { resource: ['note'], operation: ['create'] } },
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
displayName: 'New Name',
|
||||
name: 'newName',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['note'], operation: ['rename'] } },
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Folder',
|
||||
name: 'folder',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['note'], operation: ['files'] } },
|
||||
default: '',
|
||||
placeholder: 'Inbox',
|
||||
},
|
||||
|
||||
// Daily note operations
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['daily'] } },
|
||||
options: [
|
||||
{ name: 'Append', value: 'dailyAppend' },
|
||||
{ name: 'Path', value: 'dailyPath' },
|
||||
{ name: 'Prepend', value: 'dailyPrepend' },
|
||||
{ name: 'Read', value: 'dailyRead' },
|
||||
],
|
||||
default: 'dailyAppend',
|
||||
},
|
||||
{
|
||||
displayName: 'Content',
|
||||
name: 'dailyContent',
|
||||
type: 'string',
|
||||
typeOptions: { rows: 8 },
|
||||
displayOptions: { show: { resource: ['daily'], operation: ['dailyAppend', 'dailyPrepend'] } },
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
|
||||
// Property operations
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['property'] } },
|
||||
options: [
|
||||
{ name: 'List', value: 'properties' },
|
||||
{ name: 'Read', value: 'propertyRead' },
|
||||
{ name: 'Remove', value: 'propertyRemove' },
|
||||
{ name: 'Set', value: 'propertySet' },
|
||||
],
|
||||
default: 'propertySet',
|
||||
},
|
||||
{
|
||||
displayName: 'Path',
|
||||
name: 'propertyPath',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['propertyRead', 'propertyRemove', 'propertySet'] } },
|
||||
default: '',
|
||||
placeholder: 'Inbox/Note.md',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'propertyName',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['propertyRead', 'propertyRemove', 'propertySet', 'properties'] } },
|
||||
default: '',
|
||||
placeholder: 'tags',
|
||||
description: 'Frontmatter/property name, e.g. tags, date, description',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'propertyValue',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['propertySet'] } },
|
||||
default: '',
|
||||
placeholder: 'kontext, feedback-browser-cli-tests',
|
||||
description: 'Value passed to the official CLI property:set command',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'propertyType',
|
||||
type: 'options',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['propertySet'] } },
|
||||
options: [
|
||||
{ name: 'Text', value: 'text' },
|
||||
{ name: 'List', value: 'list' },
|
||||
{ name: 'Number', value: 'number' },
|
||||
{ name: 'Checkbox', value: 'checkbox' },
|
||||
{ name: 'Date', value: 'date' },
|
||||
{ name: 'Date Time', value: 'datetime' },
|
||||
],
|
||||
default: 'text',
|
||||
description: 'Obsidian property type. Use List for tags and Date for date.',
|
||||
},
|
||||
{
|
||||
displayName: 'Format',
|
||||
name: 'propertiesFormat',
|
||||
type: 'options',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['properties'] } },
|
||||
options: [
|
||||
{ name: 'YAML', value: 'yaml' },
|
||||
{ name: 'JSON', value: 'json' },
|
||||
{ name: 'TSV', value: 'tsv' },
|
||||
],
|
||||
default: 'yaml',
|
||||
},
|
||||
|
||||
// Search operations
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['search'] } },
|
||||
options: [
|
||||
{ name: 'Search', value: 'search' },
|
||||
{ name: 'Search With Context', value: 'searchContext' },
|
||||
],
|
||||
default: 'search',
|
||||
},
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['search'] } },
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Format',
|
||||
name: 'format',
|
||||
type: 'options',
|
||||
displayOptions: { show: { resource: ['search'] } },
|
||||
options: [
|
||||
{ name: 'Text', value: 'text' },
|
||||
{ name: 'JSON', value: 'json' },
|
||||
],
|
||||
default: 'text',
|
||||
},
|
||||
|
||||
// Vault operations
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['vault'] } },
|
||||
options: [
|
||||
{ name: 'Info', value: 'vaultInfo' },
|
||||
{ name: 'List Vaults', value: 'vaults' },
|
||||
],
|
||||
default: 'vaultInfo',
|
||||
},
|
||||
|
||||
// Raw command
|
||||
{
|
||||
displayName: 'Args JSON',
|
||||
name: 'rawArgs',
|
||||
type: 'json',
|
||||
displayOptions: { show: { resource: ['raw'] } },
|
||||
default: '["version"]',
|
||||
description: 'Structured CLI args array. Example: ["create", "path=Inbox/Test.md", "content=Hello", "overwrite"]. Do not include shell strings.',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Timeout Seconds',
|
||||
name: 'timeout',
|
||||
type: 'number',
|
||||
default: 60,
|
||||
description: 'Command timeout in seconds',
|
||||
},
|
||||
{
|
||||
displayName: 'Vault',
|
||||
name: 'vault',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'work',
|
||||
description: 'Optional vault name/path. Overrides the credential default vault. Sent as vault=<value>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Verbose',
|
||||
name: 'verbose',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Only used by the List Vaults operation',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
description: INodeTypeDescription = nodeDescription;
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
@@ -304,98 +24,10 @@ export class ObsidianApi implements INodeType {
|
||||
const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as Record<string, unknown>;
|
||||
const vault = String(additionalFields.vault || '');
|
||||
const timeout = Number(additionalFields.timeout ?? 60);
|
||||
|
||||
let args: string[];
|
||||
|
||||
switch (resource) {
|
||||
case 'note': {
|
||||
args = buildNoteArgs(this, operation, itemIndex);
|
||||
break;
|
||||
}
|
||||
case 'daily': {
|
||||
args = buildDailyArgs(this, operation, itemIndex);
|
||||
break;
|
||||
}
|
||||
case 'search': {
|
||||
args = buildSearchArgs(this, operation, itemIndex);
|
||||
break;
|
||||
}
|
||||
case 'property': {
|
||||
args = buildPropertyArgs(this, operation, itemIndex);
|
||||
break;
|
||||
}
|
||||
case 'vault': {
|
||||
args = buildVaultArgs(this, operation, itemIndex, additionalFields);
|
||||
break;
|
||||
}
|
||||
case 'raw': {
|
||||
args = parseRawArgs(this, itemIndex);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
throw new NodeOperationError(this.getNode(), `Unsupported resource: ${resource}`, { itemIndex });
|
||||
}
|
||||
|
||||
const args = buildArgs(this, resource, operation, itemIndex, additionalFields);
|
||||
const result = await executeObsidianCommand(this, itemIndex, args, { vault, timeout });
|
||||
|
||||
// Always keep n8n output minimal/clean.
|
||||
// - Prefer parsed stdout (e.g. vault/vaults)
|
||||
// - Otherwise keep stdout as-is
|
||||
// Build a stable, minimal output shape for n8n.
|
||||
// We try to expose the single most useful field name per operation.
|
||||
const stdout = result.stdout ?? '';
|
||||
|
||||
// Daily notes
|
||||
if (resource === 'daily' && operation === 'dailyPath') {
|
||||
const out: any = { path: stdout };
|
||||
if (result.stderr) out.stderr = result.stderr;
|
||||
if (result.timed_out) out.timed_out = result.timed_out;
|
||||
returnData.push({ json: out, pairedItem: { item: itemIndex } });
|
||||
continue;
|
||||
}
|
||||
if (resource === 'daily' && operation === 'dailyRead') {
|
||||
const out: any = { content: stdout };
|
||||
if (result.stderr) out.stderr = result.stderr;
|
||||
if (result.timed_out) out.timed_out = result.timed_out;
|
||||
returnData.push({ json: out, pairedItem: { item: itemIndex } });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Notes
|
||||
if (resource === 'note' && operation === 'read') {
|
||||
const out: any = { content: stdout };
|
||||
if (result.stderr) out.stderr = result.stderr;
|
||||
if (result.timed_out) out.timed_out = result.timed_out;
|
||||
returnData.push({ json: out, pairedItem: { item: itemIndex } });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Properties: keep CLI output because read/list return useful data.
|
||||
if (resource === 'property' && (operation === 'propertyRead' || operation === 'properties')) {
|
||||
const out: any = { content: stdout };
|
||||
if (result.stderr) out.stderr = result.stderr;
|
||||
if (result.timed_out) out.timed_out = result.timed_out;
|
||||
returnData.push({ json: out, pairedItem: { item: itemIndex } });
|
||||
continue;
|
||||
}
|
||||
|
||||
// vault and vaults are parsed as structured JSON
|
||||
const parsed = (result as any).parsed_stdout;
|
||||
if (parsed) {
|
||||
const out: any = parsed;
|
||||
if (result.timed_out) out.timed_out = result.timed_out;
|
||||
if (result.stderr) out.stderr = result.stderr;
|
||||
returnData.push({ json: out, pairedItem: { item: itemIndex } });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Everything else (writes, deletes, rename, list files, etc.):
|
||||
// keep output clean/stable and only confirm success.
|
||||
const out: any = { ok: true };
|
||||
if (result.stderr) out.stderr = result.stderr;
|
||||
if (result.timed_out) out.timed_out = result.timed_out;
|
||||
|
||||
returnData.push({ json: out, pairedItem: { item: itemIndex } });
|
||||
returnData.push(outputForResult(result, resource, operation, itemIndex));
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
@@ -409,100 +41,3 @@ export class ObsidianApi implements INodeType {
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
|
||||
function buildNoteArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
const path = context.getNodeParameter('path', itemIndex, '') as string;
|
||||
switch (operation) {
|
||||
case 'create':
|
||||
return compactArgs(['create', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string), kv('overwrite', context.getNodeParameter('overwrite', itemIndex) as boolean)]);
|
||||
case 'read':
|
||||
return compactArgs(['read', kv('path', path)]);
|
||||
case 'append':
|
||||
return compactArgs(['append', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string)]);
|
||||
case 'prepend':
|
||||
return compactArgs(['prepend', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string)]);
|
||||
case 'delete':
|
||||
return compactArgs(['delete', kv('path', path)]);
|
||||
case 'rename':
|
||||
return compactArgs(['rename', kv('path', path), kv('name', context.getNodeParameter('newName', itemIndex) as string)]);
|
||||
case 'files':
|
||||
return compactArgs(['files', kv('folder', context.getNodeParameter('folder', itemIndex, '') as string)]);
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported note operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function buildDailyArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
switch (operation) {
|
||||
case 'dailyAppend':
|
||||
return compactArgs(['daily:append', kv('content', context.getNodeParameter('dailyContent', itemIndex) as string)]);
|
||||
case 'dailyPrepend':
|
||||
return compactArgs(['daily:prepend', kv('content', context.getNodeParameter('dailyContent', itemIndex) as string)]);
|
||||
case 'dailyRead':
|
||||
return ['daily:read'];
|
||||
case 'dailyPath':
|
||||
return ['daily:path'];
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported daily operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function buildSearchArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
const command = operation === 'searchContext' ? 'search:context' : 'search';
|
||||
return compactArgs([
|
||||
command,
|
||||
kv('query', context.getNodeParameter('query', itemIndex) as string),
|
||||
kv('format', context.getNodeParameter('format', itemIndex) as string),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildPropertyArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
const path = context.getNodeParameter('propertyPath', itemIndex, '') as string;
|
||||
const name = context.getNodeParameter('propertyName', itemIndex, '') as string;
|
||||
|
||||
switch (operation) {
|
||||
case 'properties':
|
||||
return compactArgs([
|
||||
'properties',
|
||||
kv('name', name),
|
||||
kv('format', context.getNodeParameter('propertiesFormat', itemIndex) as string),
|
||||
]);
|
||||
case 'propertyRead':
|
||||
return compactArgs(['property:read', kv('path', path), kv('name', name)]);
|
||||
case 'propertyRemove':
|
||||
return compactArgs(['property:remove', kv('path', path), kv('name', name)]);
|
||||
case 'propertySet':
|
||||
return compactArgs([
|
||||
'property:set',
|
||||
kv('path', path),
|
||||
kv('name', name),
|
||||
kv('value', context.getNodeParameter('propertyValue', itemIndex) as string),
|
||||
kv('type', context.getNodeParameter('propertyType', itemIndex) as string),
|
||||
]);
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported property operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function buildVaultArgs(context: IExecuteFunctions, operation: string, itemIndex: number, additionalFields: Record<string, unknown>): string[] {
|
||||
switch (operation) {
|
||||
case 'vaultInfo':
|
||||
return ['vault'];
|
||||
case 'vaults': {
|
||||
const verbose = additionalFields.verbose === undefined ? true : Boolean(additionalFields.verbose);
|
||||
return compactArgs(['vaults', kv('verbose', verbose)]);
|
||||
}
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported vault operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function parseRawArgs(context: IExecuteFunctions, itemIndex: number): string[] {
|
||||
const raw = context.getNodeParameter('rawArgs', itemIndex) as string | string[];
|
||||
const parsed = Array.isArray(raw) ? raw : JSON.parse(raw);
|
||||
if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'string')) {
|
||||
throw new NodeOperationError(context.getNode(), 'Args JSON must be an array of strings', { itemIndex });
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { compactArgs, kv } from '../../shared/ObsidianApiClient';
|
||||
|
||||
export function buildArgs(context: IExecuteFunctions, resource: string, operation: string, itemIndex: number, additionalFields: Record<string, unknown>): string[] {
|
||||
switch (resource) {
|
||||
case 'note':
|
||||
return buildNoteArgs(context, operation, itemIndex);
|
||||
case 'daily':
|
||||
return buildDailyArgs(context, operation, itemIndex);
|
||||
case 'search':
|
||||
return buildSearchArgs(context, operation, itemIndex);
|
||||
case 'property':
|
||||
return buildPropertyArgs(context, operation, itemIndex);
|
||||
case 'vault':
|
||||
return buildVaultArgs(context, operation, itemIndex, additionalFields);
|
||||
case 'raw':
|
||||
return parseRawArgs(context, itemIndex);
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported resource: ${resource}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function buildNoteArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
const path = context.getNodeParameter('path', itemIndex, '') as string;
|
||||
switch (operation) {
|
||||
case 'create':
|
||||
return compactArgs(['create', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string), kv('overwrite', context.getNodeParameter('overwrite', itemIndex) as boolean)]);
|
||||
case 'read':
|
||||
return compactArgs(['read', kv('path', path)]);
|
||||
case 'append':
|
||||
return compactArgs(['append', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string)]);
|
||||
case 'prepend':
|
||||
return compactArgs(['prepend', kv('path', path), kv('content', context.getNodeParameter('content', itemIndex) as string)]);
|
||||
case 'delete':
|
||||
return compactArgs(['delete', kv('path', path)]);
|
||||
case 'rename':
|
||||
return compactArgs(['rename', kv('path', path), kv('name', context.getNodeParameter('newName', itemIndex) as string)]);
|
||||
case 'files':
|
||||
return compactArgs(['files', kv('folder', context.getNodeParameter('folder', itemIndex, '') as string)]);
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported note operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function buildDailyArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
switch (operation) {
|
||||
case 'dailyAppend':
|
||||
return compactArgs(['daily:append', kv('content', context.getNodeParameter('dailyContent', itemIndex) as string)]);
|
||||
case 'dailyPrepend':
|
||||
return compactArgs(['daily:prepend', kv('content', context.getNodeParameter('dailyContent', itemIndex) as string)]);
|
||||
case 'dailyRead':
|
||||
return ['daily:read'];
|
||||
case 'dailyPath':
|
||||
return ['daily:path'];
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported daily operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function buildSearchArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
const command = operation === 'searchContext' ? 'search:context' : 'search';
|
||||
return compactArgs([
|
||||
command,
|
||||
kv('query', context.getNodeParameter('query', itemIndex) as string),
|
||||
kv('format', context.getNodeParameter('format', itemIndex) as string),
|
||||
]);
|
||||
}
|
||||
|
||||
function buildPropertyArgs(context: IExecuteFunctions, operation: string, itemIndex: number): string[] {
|
||||
const path = context.getNodeParameter('propertyPath', itemIndex, '') as string;
|
||||
const name = context.getNodeParameter('propertyName', itemIndex, '') as string;
|
||||
|
||||
switch (operation) {
|
||||
case 'properties':
|
||||
return compactArgs([
|
||||
'properties',
|
||||
kv('name', name),
|
||||
kv('format', context.getNodeParameter('propertiesFormat', itemIndex) as string),
|
||||
]);
|
||||
case 'propertyRead':
|
||||
return compactArgs(['property:read', kv('path', path), kv('name', name)]);
|
||||
case 'propertyRemove':
|
||||
return compactArgs(['property:remove', kv('path', path), kv('name', name)]);
|
||||
case 'propertySet':
|
||||
return compactArgs([
|
||||
'property:set',
|
||||
kv('path', path),
|
||||
kv('name', name),
|
||||
kv('value', context.getNodeParameter('propertyValue', itemIndex) as string),
|
||||
kv('type', context.getNodeParameter('propertyType', itemIndex) as string),
|
||||
]);
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported property operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function buildVaultArgs(context: IExecuteFunctions, operation: string, itemIndex: number, additionalFields: Record<string, unknown>): string[] {
|
||||
switch (operation) {
|
||||
case 'vaultInfo':
|
||||
return ['vault'];
|
||||
case 'vaults': {
|
||||
const verbose = additionalFields.verbose === undefined ? true : Boolean(additionalFields.verbose);
|
||||
return compactArgs(['vaults', kv('verbose', verbose)]);
|
||||
}
|
||||
default:
|
||||
throw new NodeOperationError(context.getNode(), `Unsupported vault operation: ${operation}`, { itemIndex });
|
||||
}
|
||||
}
|
||||
|
||||
function parseRawArgs(context: IExecuteFunctions, itemIndex: number): string[] {
|
||||
const raw = context.getNodeParameter('rawArgs', itemIndex) as string | string[];
|
||||
const parsed = Array.isArray(raw) ? raw : JSON.parse(raw);
|
||||
if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === 'string')) {
|
||||
throw new NodeOperationError(context.getNode(), 'Args JSON must be an array of strings', { itemIndex });
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import type { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
export const nodeDescription: INodeTypeDescription = {
|
||||
displayName: 'Obsidian API',
|
||||
name: 'obsidianApi',
|
||||
icon: 'file:obsidian.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"]}}',
|
||||
description: 'Run official Obsidian CLI commands through the Obsidian API Docker service',
|
||||
defaults: {
|
||||
name: 'Obsidian API',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [
|
||||
{
|
||||
name: 'obsidianApi',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{ name: 'Daily Note', value: 'daily' },
|
||||
{ name: 'Note', value: 'note' },
|
||||
{ name: 'Property', value: 'property' },
|
||||
{ name: 'Raw Command', value: 'raw' },
|
||||
{ name: 'Search', value: 'search' },
|
||||
{ name: 'Vault', value: 'vault' },
|
||||
],
|
||||
default: 'note',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['note'] } },
|
||||
options: [
|
||||
{ name: 'Append', value: 'append' },
|
||||
{ name: 'Create', value: 'create' },
|
||||
{ name: 'Delete', value: 'delete' },
|
||||
{ name: 'List Files', value: 'files' },
|
||||
{ name: 'Prepend', value: 'prepend' },
|
||||
{ name: 'Read', value: 'read' },
|
||||
{ name: 'Rename', value: 'rename' },
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
{
|
||||
displayName: 'Path',
|
||||
name: 'path',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['note'], operation: ['append', 'create', 'delete', 'prepend', 'read', 'rename'] } },
|
||||
default: '',
|
||||
placeholder: 'Inbox/Note.md',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Content',
|
||||
name: 'content',
|
||||
type: 'string',
|
||||
typeOptions: { rows: 8 },
|
||||
displayOptions: { show: { resource: ['note'], operation: ['append', 'create', 'prepend'] } },
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Overwrite',
|
||||
name: 'overwrite',
|
||||
type: 'boolean',
|
||||
displayOptions: { show: { resource: ['note'], operation: ['create'] } },
|
||||
default: false,
|
||||
},
|
||||
{
|
||||
displayName: 'New Name',
|
||||
name: 'newName',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['note'], operation: ['rename'] } },
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Folder',
|
||||
name: 'folder',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['note'], operation: ['files'] } },
|
||||
default: '',
|
||||
placeholder: 'Inbox',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['daily'] } },
|
||||
options: [
|
||||
{ name: 'Append', value: 'dailyAppend' },
|
||||
{ name: 'Path', value: 'dailyPath' },
|
||||
{ name: 'Prepend', value: 'dailyPrepend' },
|
||||
{ name: 'Read', value: 'dailyRead' },
|
||||
],
|
||||
default: 'dailyAppend',
|
||||
},
|
||||
{
|
||||
displayName: 'Content',
|
||||
name: 'dailyContent',
|
||||
type: 'string',
|
||||
typeOptions: { rows: 8 },
|
||||
displayOptions: { show: { resource: ['daily'], operation: ['dailyAppend', 'dailyPrepend'] } },
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['property'] } },
|
||||
options: [
|
||||
{ name: 'List', value: 'properties' },
|
||||
{ name: 'Read', value: 'propertyRead' },
|
||||
{ name: 'Remove', value: 'propertyRemove' },
|
||||
{ name: 'Set', value: 'propertySet' },
|
||||
],
|
||||
default: 'propertySet',
|
||||
},
|
||||
{
|
||||
displayName: 'Path',
|
||||
name: 'propertyPath',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['propertyRead', 'propertyRemove', 'propertySet'] } },
|
||||
default: '',
|
||||
placeholder: 'Inbox/Note.md',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Name',
|
||||
name: 'propertyName',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['propertyRead', 'propertyRemove', 'propertySet', 'properties'] } },
|
||||
default: '',
|
||||
placeholder: 'tags',
|
||||
description: 'Frontmatter/property name, e.g. tags, date, description',
|
||||
},
|
||||
{
|
||||
displayName: 'Value',
|
||||
name: 'propertyValue',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['propertySet'] } },
|
||||
default: '',
|
||||
placeholder: 'kontext, feedback-browser-cli-tests',
|
||||
description: 'Value passed to the official CLI property:set command',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Type',
|
||||
name: 'propertyType',
|
||||
type: 'options',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['propertySet'] } },
|
||||
options: [
|
||||
{ name: 'Text', value: 'text' },
|
||||
{ name: 'List', value: 'list' },
|
||||
{ name: 'Number', value: 'number' },
|
||||
{ name: 'Checkbox', value: 'checkbox' },
|
||||
{ name: 'Date', value: 'date' },
|
||||
{ name: 'Date Time', value: 'datetime' },
|
||||
],
|
||||
default: 'text',
|
||||
description: 'Obsidian property type. Use List for tags and Date for date.',
|
||||
},
|
||||
{
|
||||
displayName: 'Format',
|
||||
name: 'propertiesFormat',
|
||||
type: 'options',
|
||||
displayOptions: { show: { resource: ['property'], operation: ['properties'] } },
|
||||
options: [
|
||||
{ name: 'YAML', value: 'yaml' },
|
||||
{ name: 'JSON', value: 'json' },
|
||||
{ name: 'TSV', value: 'tsv' },
|
||||
],
|
||||
default: 'yaml',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['search'] } },
|
||||
options: [
|
||||
{ name: 'Search', value: 'search' },
|
||||
{ name: 'Search With Context', value: 'searchContext' },
|
||||
],
|
||||
default: 'search',
|
||||
},
|
||||
{
|
||||
displayName: 'Query',
|
||||
name: 'query',
|
||||
type: 'string',
|
||||
displayOptions: { show: { resource: ['search'] } },
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Format',
|
||||
name: 'format',
|
||||
type: 'options',
|
||||
displayOptions: { show: { resource: ['search'] } },
|
||||
options: [
|
||||
{ name: 'Text', value: 'text' },
|
||||
{ name: 'JSON', value: 'json' },
|
||||
],
|
||||
default: 'text',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: { show: { resource: ['vault'] } },
|
||||
options: [
|
||||
{ name: 'Info', value: 'vaultInfo' },
|
||||
{ name: 'List Vaults', value: 'vaults' },
|
||||
],
|
||||
default: 'vaultInfo',
|
||||
},
|
||||
{
|
||||
displayName: 'Args JSON',
|
||||
name: 'rawArgs',
|
||||
type: 'json',
|
||||
displayOptions: { show: { resource: ['raw'] } },
|
||||
default: '["version"]',
|
||||
description: 'Structured CLI args array. Example: ["create", "path=Inbox/Test.md", "content=Hello", "overwrite"]. Do not include shell strings.',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Timeout Seconds',
|
||||
name: 'timeout',
|
||||
type: 'number',
|
||||
default: 60,
|
||||
description: 'Command timeout in seconds',
|
||||
},
|
||||
{
|
||||
displayName: 'Vault',
|
||||
name: 'vault',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'work',
|
||||
description: 'Optional vault name/path. Overrides the credential default vault. Sent as vault=<value>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Verbose',
|
||||
name: 'verbose',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Only used by the List Vaults operation',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
|
||||
import type { ObsidianCommandResult } from '../../shared/ObsidianApiClient';
|
||||
|
||||
export function outputForResult(result: ObsidianCommandResult, resource: string, operation: string, itemIndex: number): INodeExecutionData {
|
||||
const stdout = result.stdout ?? '';
|
||||
|
||||
if (resource === 'daily' && operation === 'dailyPath') {
|
||||
return paired({ path: stdout }, result, itemIndex);
|
||||
}
|
||||
if (resource === 'daily' && operation === 'dailyRead') {
|
||||
return paired({ content: stdout }, result, itemIndex);
|
||||
}
|
||||
if (resource === 'note' && operation === 'read') {
|
||||
return paired({ content: stdout }, result, itemIndex);
|
||||
}
|
||||
if (resource === 'property' && (operation === 'propertyRead' || operation === 'properties')) {
|
||||
return paired({ content: stdout }, result, itemIndex);
|
||||
}
|
||||
|
||||
const parsed = result.parsed_stdout;
|
||||
if (parsed) return paired(parsed, result, itemIndex);
|
||||
|
||||
return paired({ ok: true }, result, itemIndex);
|
||||
}
|
||||
|
||||
function paired(json: IDataObject, result: ObsidianCommandResult, itemIndex: number): INodeExecutionData {
|
||||
const out: IDataObject = { ...json };
|
||||
if (result.stderr) out.stderr = result.stderr;
|
||||
if (result.timed_out) out.timed_out = result.timed_out;
|
||||
return { json: out, pairedItem: { item: itemIndex } };
|
||||
}
|
||||
Reference in New Issue
Block a user