diff --git a/packages/cli/src/public-api/v1/handlers/credentials/__tests__/credentials.service.test.ts b/packages/cli/src/public-api/v1/handlers/credentials/__tests__/credentials.service.test.ts index 77489c14e3d..6fa4605a607 100644 --- a/packages/cli/src/public-api/v1/handlers/credentials/__tests__/credentials.service.test.ts +++ b/packages/cli/src/public-api/v1/handlers/credentials/__tests__/credentials.service.test.ts @@ -6,6 +6,255 @@ import { toJsonSchema } from '../credentials.service'; describe('CredentialsService', () => { describe('toJsonSchema', () => { + it('should create separate conditionals for different values of the same dependant field', () => { + // This test simulates the JWT auth credential scenario where + // multiple properties depend on the same field (keyType) but with different values + const properties: INodeProperties[] = [ + { + name: 'keyType', + type: 'options', + options: [ + { value: 'passphrase', name: 'Passphrase' }, + { value: 'pemKey', name: 'PEM Key' }, + ], + displayName: 'Key Type', + default: 'passphrase', + }, + { + name: 'secret', + type: 'string', + displayName: 'Secret', + default: '', + displayOptions: { + show: { + keyType: ['passphrase'], + }, + }, + }, + { + name: 'privateKey', + type: 'string', + displayName: 'Private Key', + default: '', + displayOptions: { + show: { + keyType: ['pemKey'], + }, + }, + }, + { + name: 'publicKey', + type: 'string', + displayName: 'Public Key', + default: '', + displayOptions: { + show: { + keyType: ['pemKey'], + }, + }, + }, + ]; + + const schema = toJsonSchema(properties); + + const props = schema.properties as IDataObject; + expect(props).toBeDefined(); + expect(props.keyType).toEqual({ + type: 'string', + enum: ['passphrase', 'pemKey'], + }); + + // All conditional fields should not be globally required + expect(schema.required).not.toContain('secret'); + expect(schema.required).not.toContain('privateKey'); + expect(schema.required).not.toContain('publicKey'); + + // Should have 2 separate conditionals (one for each keyType value) + const allOf = schema.allOf as GenericValue[] | IDataObject[]; + expect(Array.isArray(allOf)).toBe(true); + expect(allOf?.length).toBe(2); + + // Find conditional for passphrase + const passphraseCondition = allOf?.find( + (cond) => (cond as any).if?.properties?.keyType?.enum?.[0] === 'passphrase', + ) as IDependency; + expect(passphraseCondition).toBeDefined(); + expect(passphraseCondition.then?.allOf).toHaveLength(1); + expect(passphraseCondition.then?.allOf[0].required).toContain('secret'); + expect(passphraseCondition.then?.allOf[0].required).not.toContain('privateKey'); + expect(passphraseCondition.then?.allOf[0].required).not.toContain('publicKey'); + + // Find conditional for pemKey + const pemKeyCondition = allOf?.find( + (cond) => (cond as any).if?.properties?.keyType?.enum?.[0] === 'pemKey', + ) as IDependency; + expect(pemKeyCondition).toBeDefined(); + expect(pemKeyCondition.then?.allOf).toHaveLength(2); + expect( + pemKeyCondition.then?.allOf.some((req: any) => req.required?.includes('privateKey')), + ).toBe(true); + expect( + pemKeyCondition.then?.allOf.some((req: any) => req.required?.includes('publicKey')), + ).toBe(true); + expect(pemKeyCondition.then?.allOf.some((req: any) => req.required?.includes('secret'))).toBe( + false, + ); + }); + + it('should handle properties with no displayOptions as globally required', () => { + const properties: INodeProperties[] = [ + { name: 'apiKey', type: 'string', required: true, displayName: 'API Key', default: '' }, + { name: 'domain', type: 'string', required: true, displayName: 'Domain', default: '' }, + { + name: 'optionalField', + type: 'string', + required: false, + displayName: 'Optional', + default: '', + }, + ]; + + const schema = toJsonSchema(properties); + + expect(schema.required).toEqual(expect.arrayContaining(['apiKey', 'domain'])); + expect(schema.required).not.toContain('optionalField'); + expect(schema.allOf).toBeUndefined(); + }); + + it('should handle mix of required and conditional properties', () => { + const properties: INodeProperties[] = [ + { name: 'apiKey', type: 'string', required: true, displayName: 'API Key', default: '' }, + { + name: 'authType', + type: 'options', + required: true, + options: [ + { value: 'basic', name: 'Basic' }, + { value: 'oauth2', name: 'OAuth2' }, + ], + displayName: 'Auth Type', + default: 'basic', + }, + { + name: 'username', + type: 'string', + displayName: 'Username', + default: '', + displayOptions: { + show: { + authType: ['basic'], + }, + }, + }, + { + name: 'password', + type: 'string', + displayName: 'Password', + default: '', + displayOptions: { + show: { + authType: ['basic'], + }, + }, + }, + { + name: 'clientId', + type: 'string', + displayName: 'Client ID', + default: '', + displayOptions: { + show: { + authType: ['oauth2'], + }, + }, + }, + ]; + + const schema = toJsonSchema(properties); + + // apiKey and authType should be globally required + expect(schema.required).toEqual(expect.arrayContaining(['apiKey', 'authType'])); + // Conditional fields should not be globally required + expect(schema.required).not.toContain('username'); + expect(schema.required).not.toContain('password'); + expect(schema.required).not.toContain('clientId'); + + // Should have 2 conditionals + const allOf = schema.allOf as GenericValue[] | IDataObject[]; + expect(allOf?.length).toBe(2); + }); + + it('should handle properties with multiple options depending on same field', () => { + const properties: INodeProperties[] = [ + { + name: 'operation', + type: 'options', + options: [ + { value: 'create', name: 'Create' }, + { value: 'update', name: 'Update' }, + { value: 'delete', name: 'Delete' }, + ], + displayName: 'Operation', + default: 'create', + }, + { + name: 'createField', + type: 'string', + displayName: 'Create Field', + default: '', + displayOptions: { + show: { + operation: ['create'], + }, + }, + }, + { + name: 'updateField', + type: 'string', + displayName: 'Update Field', + default: '', + displayOptions: { + show: { + operation: ['update'], + }, + }, + }, + { + name: 'deleteField', + type: 'string', + displayName: 'Delete Field', + default: '', + displayOptions: { + show: { + operation: ['delete'], + }, + }, + }, + ]; + + const schema = toJsonSchema(properties); + + // Should have 3 separate conditionals (one for each operation) + const allOf = schema.allOf as GenericValue[] | IDataObject[]; + expect(allOf?.length).toBe(3); + + // Verify each conditional is correct + const createCondition = allOf?.find( + (cond) => (cond as any).if?.properties?.operation?.enum?.[0] === 'create', + ) as IDependency; + expect(createCondition?.then?.allOf[0].required).toContain('createField'); + + const updateCondition = allOf?.find( + (cond) => (cond as any).if?.properties?.operation?.enum?.[0] === 'update', + ) as IDependency; + expect(updateCondition?.then?.allOf[0].required).toContain('updateField'); + + const deleteCondition = allOf?.find( + (cond) => (cond as any).if?.properties?.operation?.enum?.[0] === 'delete', + ) as IDependency; + expect(deleteCondition?.then?.allOf[0].required).toContain('deleteField'); + }); + it('should add "false" displayOptions.show dependant value as allof condition', () => { const properties: INodeProperties[] = [ { name: 'field1', type: 'string', required: true, displayName: 'Field 1', default: '' }, diff --git a/packages/cli/src/public-api/v1/handlers/credentials/credentials.service.ts b/packages/cli/src/public-api/v1/handlers/credentials/credentials.service.ts index a9050406557..8b1659b6a15 100644 --- a/packages/cli/src/public-api/v1/handlers/credentials/credentials.service.ts +++ b/packages/cli/src/public-api/v1/handlers/credentials/credentials.service.ts @@ -215,11 +215,12 @@ export function toJsonSchema(properties: INodeProperties[]): IDataObject { dependantValue = displayOptionsValues[0]; } - if (propertyRequiredDependencies[dependantName] === undefined) { - propertyRequiredDependencies[dependantName] = {}; - } + // Create a unique key for each dependant name and value combination + // so that if multiple properties depend on the same property but different values + // they get their own if-then-else block + const dependencyKey = `${dependantName}:${JSON.stringify(dependantValue)}`; - if (!resolveProperties.includes(dependantName)) { + if (!resolveProperties.includes(dependencyKey)) { let conditionalValue; if (typeof dependantValue === 'object' && dependantValue._cnd) { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -285,7 +286,7 @@ export function toJsonSchema(properties: INodeProperties[]): IDataObject { enum: [dependantValue], }; } - propertyRequiredDependencies[dependantName] = { + propertyRequiredDependencies[dependencyKey] = { if: { properties: { [dependantName]: conditionalValue, @@ -298,14 +299,13 @@ export function toJsonSchema(properties: INodeProperties[]): IDataObject { allOf: [], }, }; + resolveProperties.push(dependencyKey); } - propertyRequiredDependencies[dependantName].then?.allOf.push({ required: [property.name] }); - propertyRequiredDependencies[dependantName].else?.allOf.push({ + propertyRequiredDependencies[dependencyKey].then?.allOf.push({ required: [property.name] }); + propertyRequiredDependencies[dependencyKey].else?.allOf.push({ not: { required: [property.name] }, }); - - resolveProperties.push(dependantName); // remove global required requiredFields = requiredFields.filter((field) => field !== property.name); }