D’après les éléments trouvés, en version 2022.1 vous ne disposez pas de la méthode permettant d’ajouter plusieurs labels sur une métrique custom via l’extension SAM. La méthode mentionnée pour cela est SetSensorLabels, mais elle est citée comme solution disponible dans une version plus récente, et l’échange précise explicitement que ce n’est pas disponible en 2022.1. [1]

Dans le cas présenté, la conclusion donnée est qu’ils ont mis à niveau leur version IRIS pour obtenir cette possibilité. [1]

Donc, pour votre besoin d’ajouter par exemple un label production à une métrique custom comme my_iris_queue, la seule piste documentée retrouvée ici est :

  • utiliser SetSensorLabels si vous êtes sur une version qui la fournit ;
  • en 2022.1, cette possibilité n’est pas indiquée comme disponible ;
  • la solution retenue dans l’échange a été l’upgrade de la version IRIS. [1]


Sources:

Oui, %INLIST est prévu pour recevoir une liste au format ObjectScript %List / $LB(...) comme paramètre unique dans du SQL dynamique ; ce n’est donc pas, d’après les éléments retrouvés, une limitation connue au sens où plusieurs valeurs seraient interdites avec ?. Les exemples montrent explicitement WHERE Home_State %INLIST ? puis l’exécution avec une liste construite par $LISTBUILD(...), ainsi que and Channel %INLIST ? avec un argument de type %List. [1][2]

La manière correcte de le passer en SQL dynamique est donc de fournir au placeholder ? une vraie liste ObjectScript, par exemple un $LB(...), au moment du %Execute / %ExecDirect. Les documents montrent aussi que, si vous avez plusieurs paramètres dynamiques, il faut les stocker dans un tableau local indexé numériquement puis appeler %Execute(args...) ou %ExecDirect(,sql,args...). Pour le paramètre correspondant à %INLIST, l’élément du tableau doit être la liste elle-même, et non une liste imbriquée dans une autre structure de type $LIST passée comme unique argument par erreur. [1][2]

Autrement dit, la solution recommandée n’est pas de générer manuellement IN (...) dans la chaîne SQL avec concaténation de valeurs, mais de conserver %INLIST ? et de passer correctement la liste en paramètre, ce qui est aussi présenté comme l’approche sûre et maintenable pour éviter l’injection SQL. [1][3]

Un point relevé dans les exemples et échanges retrouvés est que des problèmes surviennent facilement quand on passe les paramètres avec une mauvaise arité ou un mauvais emballage de liste. Un exemple montre qu’il faut utiliser la syntaxe variadique ... pour transmettre le tableau de paramètres, et un autre commentaire souligne qu’un « embedded list » peut faire que %INLIST lise une liste là où on lui transmet en réalité une liste de listes. [1][2]

Concernant SIZE ((nn)), la documentation retrouvée rappelle seulement la syntaxe scalar-expression %INLIST list [SIZE ((nn))], avec un exemple SIZE ((10)), mais ne fournit pas ici d’indication disant que SIZE serait obligatoire ni qu’il corrigerait un problème de non-retour de lignes dans votre cas. [4][2]

En résumé, au vu des documents retrouvés :

  • %INLIST ? avec plusieurs valeurs est un usage normal si le paramètre est une vraie %List / $LB(...). [1][4][2]
  • Si la requête ne retourne rien, il faut surtout vérifier la forme exacte du paramètre transmis à %Execute, notamment qu’il ne s’agit pas d’une liste imbriquée ou d’un mauvais passage des arguments dynamiques. [1][2]
  • Construire IN (...) directement dans le texte SQL n’est pas présenté comme la seule solution ; au contraire, l’usage des paramètres avec %INLIST est la voie recommandée. [1][3]


Sources:

Vous ne devez pas créer de route /login dans votre UrlMap : pour une application REST, la méthode Login est appelée implicitement lorsqu’une authentification est requise. Pour ajouter votre traitement, il faut redéfinir Login() dans votre classe qui étend %CSP.REST et appeler le parent avec ##super(skipheader), par exemple : [1][2]

ClassMethod Login(skipheader As %Boolean = 1) As %Status [ ProcedureBlock = 0 ]
{
    // votre journalisation applicative
    Quit ##super(skipheader)
}

Le fait que votre méthode ne soit pas appelée peut venir de la configuration de l’application web REST. Il a été indiqué qu’il faut laisser seulement l’authentification par mot de passe pour cette web application, retirer éventuellement “group by id”, vérifier que UnknownUser n’a pas %ALL, puis tester l’accès ; dans cette configuration, la méthode Login doit être exécutée. Dans votre cas, l’usage du JWT est explicitement mentionné comme différence de configuration. [1][2]

OnPreDispatch n’est pas adapté pour /login, /refresh et /logout : il fonctionne pour vos routes personnalisées, mais pas pour ces routes spéciales. [1][2]

Si vous avez besoin d’un point d’extension fiable autour du login, la piste proposée est d’utiliser une sous-classe de %CSP.SessionEvents, en particulier OnLogin, ou éventuellement OnStartRequest / OnEndRequest selon le besoin de journalisation. [1][2][3]


Sources:

D’après les éléments retrouvés, il n’y a pas d’indication qu’il soit possible de surcharger les méthodes internes standard de connexion comme UsersCSPLogin() ni d’intercepter la requête avant que ce traitement standard ne s’exécute dans le scénario que vous décrivez. Le contenu retrouvé montre au contraire que, lors d’un échec de connexion CSP, la pile passe bien par UsersCSPLogin dans %SYS.SECURITY, puis par CSPLogin dans %SYS.cspServer, ce qui correspond à la validation standard côté serveur que vous observez. [1][2]

Le fait que OnPreHTTP() ne soit jamais appelé pendant ce processus est cohérent avec ce qui est décrit : OnPreHTTP() est un point d’entrée d’une page CSP, invoqué avant l’envoi des en-têtes HTTP pour cette page, et il est typiquement utilisé pour une authentification privée dans une page CSP classique. Mais dans votre cas, le traitement de connexion standard semble se produire avant d’atteindre ce point pour votre page personnalisée. [1][3]

La seule piste concrète proposée dans les documents retrouvés pour obtenir un comportement de « réactivation après 15 minutes » consiste à ne pas essayer de modifier le flux interne de login standard, mais plutôt à mettre en place une tâche personnalisée exécutée périodiquement, par exemple toutes les 15 minutes, afin de réactiver les utilisateurs désactivés. Il est aussi précisé que cette approche serait plus universelle, car il existe d’autres modes de connexion que CSP. [1]

Une autre suggestion mentionnée est de ne pas utiliser l’option Disable account if login limit reached, et éventuellement de jouer uniquement sur Invalid login limit si cela répond au besoin fonctionnel. [1]

Enfin, un autre document rappelle que pour une page de login CSP personnalisée, l’utilisateur du Web Gateway CSPSystem doit avoir les droits de lecture sur la base où se trouve la classe de login personnalisée ; sinon la page personnalisée peut ne pas être chargée correctement et %CSP.Login standard continue d’être utilisée. [4]

En résumé, les documents retrouvés ne donnent pas de mécanisme pour surcharger UsersCSPLogin() ou injecter votre logique juste avant son exécution ; la solution suggérée dans ces sources est plutôt de gérer la réactivation des comptes par une tâche planifiée externe au flux de connexion standard. [1][4][2]


Sources:

L’erreur iris.cls: error finding class indique généralement que le framework IoP n’est pas chargé dans IRIS. Il faut d’abord installer le package Python, puis l’initialiser dans IRIS : pip install iris-pex-embedded-python, puis iop --init ou, en Python, from iop import Utils suivi de Utils.setup(). [1][2][3]

Il faut aussi vérifier que vous êtes dans le bon namespace IRIS via la variable d’environnement IRISNAMESPACE. [1][2]

Pour la migration, la commande documentée est iop --migrate /chemin/absolu/settings.py ou iop -M /tmp/settings.py. Le fichier settings.py doit être dans le même dossier que le code Python, et le chemin doit être absolu. [4][2][5][3]

Votre appel subprocess.run(["iop", "-m", "/external/src/CoreModel/Python/settings.py"], ...) ne correspond pas à la syntaxe montrée dans la documentation récupérée, où -m/-M est l’option de migration et attend le chemin absolu du fichier settings.py. [3]

Le fichier settings.py doit contenir au minimum :

  • CLASSES : le mapping des composants Python à enregistrer ;
  • PRODUCTIONS : la définition de la production et des items associés. [4][2][6]

Exemple minimal de structure :

from hello_world.bo import MyBo

CLASSES = {
    "MyIRIS.MyBo": MyBo
}

PRODUCTIONS = [
    {
        'MyIRIS.Production': {
            "@TestingEnabled": "true",
            "Item": [
                {
                    "@Name": "Instance.Of.MyBo",
                    "@ClassName": "MyIRIS.MyBo",
                }
            ]
        }
    }
]

[2][6]

Pour enregistrer des composants, la documentation indique aussi ces méthodes alternatives :

  • Utils.register_component(...)
  • Utils.register_file(...)
  • Utils.register_folder(...)
  • Utils.migrate() pour migrer le fichier settings.py. [4]

En pratique, l’enchaînement à vérifier est :

  1. installer iris-pex-embedded-python ;
  2. initialiser IoP dans IRIS avec iop --init ou Utils.setup() ;
  3. vérifier IRISNAMESPACE ;
  4. utiliser un settings.py valide avec CLASSES et PRODUCTIONS ;
  5. lancer la migration avec un chemin absolu, par exemple iop --migrate /external/src/CoreModel/Python/settings.py. [1][4][2][3]


Sources:

Oui : une approche proposée consiste à rendre la comparaison récursive pour traiter aussi les propriétés objets et les collections. Le principe est : vérifier d’abord que les deux objets sont de la même classe, parcourir les propriétés via %Library.ClassDefinition, puis :

  • si la propriété est un objet, rappeler la méthode de comparaison ;
  • si l’objet étend %Collection.Super, parcourir ses éléments avec Count() et GetAt(ii) puis comparer chaque élément ;
  • sinon, pour les valeurs simples, enregistrer les différences quand les valeurs sont distinctes. [1]

Exemple fourni : [1]

ClassMethod CompareObjects(expectedObject As %RegisteredObject, actualObject As %RegisteredObject) As %Status
{
    Set status= $$$OK
    Set returnValue = 1
    Try{
        // check if same class
        Set className = $CLASSNAME(expectedObject)
        If (className '= $CLASSNAME(actualObject)) {
            Set returnValue = 0
            Set ^||differences(expectedObject,actualObject) = "not same class"
        }
        If (returnValue) {
            // Get the definition to browse properties
            Set classDef = ##class(%Library.ClassDefinition).%OpenId(className)
            Set propertiesDef = classDef.Properties
            // Check the value for each property
            For i = 1:1:propertiesDef.Count() {
                Set propertyName = propertiesDef.GetAt(i).Name
                Set propertyValueExpected = $PROPERTY(expectedObject,propertyName)
                Set propertyValueActual=$PROPERTY(actualObject,propertyName)
                // if different, supply the variable difference with the name of property + value
                If $ISOBJECT(propertyValueExpected){
                    If propertyValueExpected.%Extends("%Collection.Super"){
                        For ii=1:1:propertyValueExpected.Count(){
                            Set tSC= ..CompareObjects(propertyValueExpected.GetAt(ii),propertyValueActual.GetAt(ii))
                        }
                    }
                    Else{
                        Set tSC= ..CompareObjects(propertyValueExpected,propertyValueActual)
                    }
                }
                Else{
                    If ((propertyValueExpected '= "") && (propertyValueExpected '= propertyValueActual)) {
                        Set ^||differences(expectedObject,propertyName)=propertyValueExpected _ " != " _ propertyValueActual
                    }
                }
            }
        }
    }Catch e{
        Set status=e.AsStatus()
    }
    Quit status
}

Pour restituer les écarts, l’exemple associé appelle ensuite CompareObjects, teste l’existence de ^||differences, puis construit un message en parcourant ce global temporaire. [1]

Pour les listes ObjectScript, il existe aussi un exemple séparé qui calcule les éléments ajoutés et supprimés entre deux %List en utilisant $LISTVALID, $LISTLENGTH, $LISTFIND et en retournant added / deleted. Cela peut être utile si certaines de vos propriétés sont des listes sérialisées plutôt que des objets de collection. [2]

Le même fil souligne aussi qu’avant de comparer des collections, il faut définir la règle métier : faut-il tenir compte de l’ordre ou non ? Deux collections contenant les mêmes valeurs dans un ordre différent peuvent être considérées soit identiques, soit différentes selon le cas d’usage. [2]

Si vous voulez fiabiliser encore votre méthode de parcours des propriétés, une autre source montre plusieurs façons d’énumérer les propriétés d’une classe, y compris héritées, notamment via %Dictionary.CompiledProperty ou %Dictionary.CompiledClass. [3]

En résumé, la solution la plus proche de votre besoin est de transformer votre méthode en comparaison récursive objet/collection, comme dans l’exemple ci-dessus. Pour les listes simples, vous pouvez compléter avec une logique spécialisée de diff de %List. [1][2][3]


Sources:

Le problème semble venir de l’utilisation de SendFormDataArray() avec une signature d’appel incorrecte. Dans la documentation, SendFormDataArray est défini ainsi :

Method SendFormDataArray(Output pHttpResponse As %Net.HttpResponse, pOp As %String, pHttpRequestIn As %Net.HttpRequest, pFormVarNames As %String = "", ByRef pData) As %Status [1]

Dans un autre exemple, il est aussi rappelé que votre appel doit respecter cette signature, et qu’un appel comme :

set tSC = ..Adapter.SendFormDataArray(.tHTTPResponse, "POST", tHTTPRequest, tURL, tPayload)

ne correspond pas à la signature attendue, car les arguments passés ne sont pas dans le bon ordre ni dans les bons paramètres. [2]

Dans votre code, vous faites :

set tSC = ..Adapter.SendFormDataArray(.response, "POST",httpRequest,,,..Adapter.URL_"/security/authentication")

or, d’après la signature documentée, SendFormDataArray() envoie la requête vers la destination configurée de l’adaptateur ; la variante documentée ici ne prend pas un paramètre pURL dans la signature affichée. [1] Cela suggère que passer l’URL complète ou concaténée en dernier argument n’est pas l’usage attendu pour cette méthode dans ce contexte. [1][3]

Si vous voulez simplement envoyer un corps de requête JSON, la documentation indique que les méthodes principales comme Post() peuvent être utilisées soit avec des variables de formulaire, soit avec un corps de requête. Pour envoyer un corps de requête au lieu de variables de formulaire, il faut laisser pFormVarNames vide et passer le corps en argument pData. Une valeur de corps peut être de type chaîne ou stream. [1] L’exemple fourni dans la documentation pour une opération HTTP sortante montre d’ailleurs l’utilisation de ..Adapter.Post(.tResponse,,input) pour envoyer un corps de requête. [1]

En pratique, la piste la plus solide est donc :

  1. ne pas passer l’URL concaténée dans cet appel SendFormDataArray() tel qu’il est écrit dans votre code ; [1]
  2. utiliser la destination configurée sur l’adaptateur (Server, Port, URL) ; [3]
  3. pour un POST JSON avec body, utiliser Post() plutôt que SendFormDataArray() si vous n’avez pas besoin d’un cas « inhabituel » de personnalisation. SendFormDataArray() est présenté comme une méthode bas niveau pour des cas particuliers. [1][3]

La documentation précise aussi que SendFormDataArray() est la méthode bas niveau à utiliser lorsque vous devez personnaliser la requête HTTP au-delà des simples variables de formulaire ou cookies, et que dans ce cas vous pouvez préparer un %Net.HttpRequest vous-même. [1][3] Mais rien dans les éléments retrouvés ne valide le passage de l’URL finale comme vous le faites ici. [1]

Donc, l’idée la plus probable est que l’erreur est déclenchée parce que l’URL est mal fournie à SendFormDataArray() dans votre appel, et que cette méthode n’est probablement pas la bonne pour poster votre JSON dans ce scénario. [2][1][3]

Vous pouvez vous orienter vers un appel de type :

Set tSC = ..Adapter.Post(.response,,entityBody)

en laissant l’URL dans la configuration de l’adaptateur, puisque Post() sait envoyer un request body lorsque pFormVarNames est vide. [1]

Si l’adresse doit varier dynamiquement, un retour d’expérience retrouvé indique qu’il a fallu positionner la destination sur l’adaptateur avant l’appel, par exemple :

set ..Adapter.HTTPServer = pRequest.Url
set URL = pRequest.Url_"/search"
set tSC = ..Adapter.SendFormDataArray(.tHttpResponse,"POST",tHttpRequest,,,URL)

car le problème venait justement d’une destination variable non configurée dans le composant. [4]


Sources: