From 1bf9f0e0fd0add9f57d0935906012157c9d70e45 Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Sun, 8 Jun 2025 15:39:14 -0700 Subject: [PATCH 01/21] feat(api): add command registry endpoint and update API configuration --- devex-ui/package-lock.json | 118 +--------------- devex-ui/src/components/CommandIssuer.tsx | 81 +++++++---- devex-ui/src/data/api.ts | 6 +- devex-ui/src/data/apiService.ts | 7 +- devex-ui/src/data/commandRegistry.ts | 126 ------------------ devex-ui/src/data/index.ts | 1 - .../src/mocks/factories/registry.factory.ts | 124 +++++++++++++++++ devex-ui/src/mocks/handlers.ts | 11 ++ devex-ui/src/mocks/scenarios/default.ts | 3 + devex-ui/src/utils/schemaValidator.ts | 15 ++- package-lock.json | 23 ++++ package.json | 1 + src/api/index.ts | 22 ++- 13 files changed, 252 insertions(+), 286 deletions(-) delete mode 100644 devex-ui/src/data/commandRegistry.ts create mode 100644 devex-ui/src/mocks/factories/registry.factory.ts diff --git a/devex-ui/package-lock.json b/devex-ui/package-lock.json index b84ce0b3..b4b0ca55 100644 --- a/devex-ui/package-lock.json +++ b/devex-ui/package-lock.json @@ -88,7 +88,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -1029,7 +1028,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -1047,7 +1045,6 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -1062,7 +1059,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1072,7 +1068,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1082,14 +1077,12 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1142,7 +1135,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1156,7 +1148,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1166,7 +1157,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -1205,7 +1195,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -3213,14 +3202,14 @@ "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.12", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz", "integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -3231,7 +3220,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -3582,7 +3571,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -3595,7 +3583,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3611,14 +3598,12 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -3632,7 +3617,6 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -3696,14 +3680,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3727,7 +3709,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -3789,7 +3770,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -3837,7 +3817,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -3862,7 +3841,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -4361,7 +4339,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4374,14 +4351,12 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -4408,7 +4383,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4422,7 +4396,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -4609,14 +4582,12 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, "license": "Apache-2.0" }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, "license": "MIT" }, "node_modules/dom-helpers": { @@ -4633,7 +4604,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, "node_modules/electron-to-chromium": { @@ -4675,7 +4645,6 @@ "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/esbuild": { @@ -4990,7 +4959,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -5007,7 +4975,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -5050,7 +5017,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -5073,7 +5039,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -5124,7 +5089,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -5161,7 +5125,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5176,7 +5139,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5205,7 +5167,6 @@ "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -5226,7 +5187,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -5239,7 +5199,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -5249,7 +5208,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -5305,7 +5263,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -5390,7 +5347,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -5403,7 +5359,6 @@ "version": "2.15.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", - "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -5419,7 +5374,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5429,7 +5383,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5439,7 +5392,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -5459,7 +5411,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -5469,14 +5420,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -5492,7 +5441,6 @@ "version": "1.21.6", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", - "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -5639,7 +5587,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -5652,7 +5599,6 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, "license": "MIT" }, "node_modules/locate-path": { @@ -6162,7 +6108,6 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, "license": "ISC" }, "node_modules/lucide-react": { @@ -6187,7 +6132,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -6197,7 +6141,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -6224,7 +6167,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -6296,7 +6238,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -6308,7 +6249,6 @@ "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -6351,7 +6291,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6380,7 +6319,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6456,7 +6394,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -6486,7 +6423,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6496,14 +6432,12 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true, "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", @@ -6527,14 +6461,12 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -6547,7 +6479,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6557,7 +6488,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -6567,7 +6497,6 @@ "version": "8.4.47", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6596,7 +6525,6 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -6614,7 +6542,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", - "dev": true, "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" @@ -6634,7 +6561,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6670,7 +6596,6 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -6696,7 +6621,6 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -6710,7 +6634,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -6774,7 +6697,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -6999,7 +6921,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -7009,7 +6930,6 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -7086,7 +7006,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", @@ -7114,7 +7033,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -7161,7 +7079,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -7207,7 +7124,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -7220,7 +7136,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7230,7 +7145,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -7253,7 +7167,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -7286,7 +7199,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -7305,7 +7217,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -7320,7 +7231,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7330,14 +7240,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7350,7 +7258,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -7367,7 +7274,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7380,7 +7286,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7403,7 +7308,6 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -7439,7 +7343,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7462,7 +7365,6 @@ "version": "3.4.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", - "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -7516,7 +7418,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", - "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -7526,7 +7427,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", - "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -7545,7 +7445,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -7587,7 +7486,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true, "license": "Apache-2.0" }, "node_modules/tslib": { @@ -7776,7 +7674,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/uuid": { @@ -7891,7 +7788,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -7917,7 +7813,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -7936,7 +7831,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -7954,7 +7848,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7964,14 +7857,12 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -7986,7 +7877,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -7999,7 +7889,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -8022,7 +7911,6 @@ "version": "2.6.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", - "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/devex-ui/src/components/CommandIssuer.tsx b/devex-ui/src/components/CommandIssuer.tsx index 6dc8e188..a82cb065 100644 --- a/devex-ui/src/components/CommandIssuer.tsx +++ b/devex-ui/src/components/CommandIssuer.tsx @@ -1,5 +1,5 @@ //devex-ui/src/components/CommandIssuer.tsx -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -9,13 +9,14 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Clock, Send, RotateCcw, Terminal, ChevronDown, ChevronRight, RefreshCw, AlertCircle } from "lucide-react"; -import { commandRegistry } from "@/data"; -import type { CommandSchema } from "@/data"; +import type { CommandSchema } from "@/data/types"; import { useCommands, useSubmitCommand } from "@/hooks/api"; -import { validate } from "@/utils/schemaValidator"; +import { validate, registerSchemas } from "@/utils/schemaValidator"; import { makeExample } from "@/utils/schemaFaker"; import { toast } from "@/components/ui/sonner"; import { useAppCtx } from '@/app/AppProvider'; +import { useQuery } from "@tanstack/react-query"; +import { fetchCommandRegistry } from "@/data/apiService"; const generateUUID4 = (): string => { return crypto.randomUUID(); @@ -35,6 +36,18 @@ export const CommandIssuer = () => { // Use React Query hooks const { data: recentCommands = [] } = useCommands(tenant, 10); const { mutate: submitCommandMutation, isPending: isSubmitting } = useSubmitCommand(); + const { data: commandRegistry = [], isLoading: isLoadingRegistry } = useQuery({ + queryKey: ['commandRegistry'], + queryFn: fetchCommandRegistry, + staleTime: Infinity + }); + + // Register schemas when registry is loaded + useEffect(() => { + if (commandRegistry.length > 0) { + registerSchemas(commandRegistry); + } + }, [commandRegistry]); // Extract field names from validation error messages const extractFieldNames = (errors: string[]): Set => { @@ -293,27 +306,34 @@ export const CommandIssuer = () => {
- + {isLoadingRegistry ? ( +
+
+ Loading command registry... +
+ ) : ( + + )} {selectedCommandSchema && (
Domain: {selectedCommandSchema.domain} @@ -344,7 +364,7 @@ export const CommandIssuer = () => {
- {selectedCommandSchema && ( + {!isLoadingRegistry && selectedCommandSchema && (

Payload

@@ -383,6 +403,12 @@ export const CommandIssuer = () => {
)} + {isLoadingRegistry && selectedCommand && ( +
+
+ Loading schema... +
+ )} {validationErrors.length > 0 && (
@@ -401,7 +427,7 @@ export const CommandIssuer = () => {
diff --git a/devex-ui/src/components/Header.tsx b/devex-ui/src/components/Header.tsx index 602593ac..1c7fa5cf 100644 --- a/devex-ui/src/components/Header.tsx +++ b/devex-ui/src/components/Header.tsx @@ -13,7 +13,7 @@ import { useAppCtx } from '@/app/AppProvider'; import { isMock, apiMode } from '@/config/apiMode'; import { Logo } from './Logo'; -const tenants = ['tenant-1', 'tenant-2', 'tenant-3']; +const tenants = ['tenant-1', 'tenant-2', 'tenant-3', '0af03580-98d5-4884-96e4-e75168d8b887']; const roles = ['admin', 'user', 'viewer']; export const Header = () => { diff --git a/src/api/routes/commands.ts b/src/api/routes/commands.ts new file mode 100644 index 00000000..4a0f1d0a --- /dev/null +++ b/src/api/routes/commands.ts @@ -0,0 +1,44 @@ +import { Router } from 'express'; +import pool from '../db'; +import { stdLogger } from '../../infra/logger/stdLogger'; + +const router = Router(); + +router.get('/api/commands', async (req, res) => { + try { + // Get query parameters + const tenantId = req.query.tenant_id as string; + const limit = req.query.limit ? Math.min(parseInt(req.query.limit as string), 1000) : 10; + + // Validate tenant_id + if (!tenantId) { + return res.status(400).json({ + error: 'tenant_id is required' + }); + } + + const client = await pool.connect(); + + try { + // Query commands from database + const result = await client.query( + `SELECT *,created_at as "createdAt", "result" as "response" FROM infra.commands + WHERE tenant_id = $1 + ORDER BY created_at DESC + LIMIT $2`, + [tenantId, limit] + ); + + res.json(result.rows); + } finally { + client.release(); + } + } catch (error) { + stdLogger.error('Commands endpoint error:', { error }); + res.status(500).json({ + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +export default router; \ No newline at end of file diff --git a/src/api/routes/index.ts b/src/api/routes/index.ts index a8cffcce..05d25d52 100644 --- a/src/api/routes/index.ts +++ b/src/api/routes/index.ts @@ -2,6 +2,7 @@ import { Express } from 'express'; import healthRoutes from './health'; import registryRoutes from './registry'; import metricsRoutes from './metrics'; +import commandsRoutes from './commands'; import accessLogMiddleware from '../middlewares/accessLog'; /** @@ -20,6 +21,9 @@ export const registerRoutes = (app: Express): void => { // Register metrics routes app.use(metricsRoutes); + + // Register commands routes + app.use(commandsRoutes); }; export default registerRoutes; From 3f899c90bda698d3b818d5820a4418a3890ef7eb Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Mon, 9 Jun 2025 18:12:38 -0700 Subject: [PATCH 07/21] feat(api): add command scheduling, noted improvements for devx in command schema contracts and singleton logging example src/core/system/payload-schemas.ts: Added aggregateId and aggregateType to payload schemas for stricter contract alignment. devex-ui/src/components/CommandIssuer.tsx: Removed unused timestamp display for cleaner UI. src/api/routes/commands.ts: Introduced a POST endpoint for scheduling commands with validation, unique ID assignment, and logging improvements. src/infra/temporal/temporal-scheduler.ts: Added warning log for unsupported commands in the scheduler for better debugging. --- devex-ui/src/components/CommandIssuer.tsx | 1 - src/api/routes/commands.ts | 107 +++++++++++++++++++++- src/core/system/payload-schemas.ts | 15 ++- src/infra/temporal/temporal-scheduler.ts | 1 + 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/devex-ui/src/components/CommandIssuer.tsx b/devex-ui/src/components/CommandIssuer.tsx index 6ddf5b64..a82cb065 100644 --- a/devex-ui/src/components/CommandIssuer.tsx +++ b/devex-ui/src/components/CommandIssuer.tsx @@ -495,7 +495,6 @@ export const CommandIssuer = () => { })} } - x{cmd.createdAt}x {cmd.id} {cmd.type}
diff --git a/src/api/routes/commands.ts b/src/api/routes/commands.ts index 4a0f1d0a..a054ffb8 100644 --- a/src/api/routes/commands.ts +++ b/src/api/routes/commands.ts @@ -1,6 +1,12 @@ import { Router } from 'express'; import pool from '../db'; -import { stdLogger } from '../../infra/logger/stdLogger'; +import {setLoggerAccessor, log} from '../../core/logger'; +import {stdLogger} from '../../infra/logger/stdLogger'; +import { TemporalScheduler } from '../../infra/temporal/temporal-scheduler'; + + +setLoggerAccessor(() => stdLogger); +const logger = log(); const router = Router(); @@ -18,7 +24,7 @@ router.get('/api/commands', async (req, res) => { } const client = await pool.connect(); - + try { // Query commands from database const result = await client.query( @@ -28,7 +34,7 @@ router.get('/api/commands', async (req, res) => { LIMIT $2`, [tenantId, limit] ); - + res.json(result.rows); } finally { client.release(); @@ -41,4 +47,97 @@ router.get('/api/commands', async (req, res) => { } }); -export default router; \ No newline at end of file +// POST endpoint for submitting commands +router.post('/api/commands', async (req, res) => { + try { + const command = req.body; + + logger?.info('Received command submission', { + operation: 'routes/commands.ts:postCommand', + commandType: command?.type, + tenantId: command?.tenant_id + }); + + // Basic validation + if (!command || !command.type || !command.tenant_id || !command.payload) { + logger?.warn('Invalid command format', { + operation: 'routes/commands.ts:postCommand', + commandType: command?.type, + tenantId: command?.tenant_id, + missingFields: [ + !command ? 'command' : null, + command && !command.type ? 'type' : null, + command && !command.tenant_id ? 'tenant_id' : null, + command && !command.payload ? 'payload' : null + ].filter(Boolean) + }); + + return res.status(400).json({ + status: 'fail', + error: 'Invalid command format. Required fields: type, tenant_id, payload' + }); + } + + // Ensure command has an ID + if (!command.id) { + command.id = require('crypto').randomUUID(); + } + + // Set initial status to pending if not provided + if (!command.status) { + command.status = 'pending'; + } + + // Create scheduler instance + const scheduler = await TemporalScheduler.create(); + + try { + // Schedule the command + console.log(command); + await scheduler.schedule(command); + + // Log success + logger?.info('Command scheduled successfully', { + operation: 'routes/commands.ts:postCommand', + commandId: command.id, + commandType: command.type, + tenantId: command.tenant_id + }); + + // Return success response + res.json({ + status: 'success', + events: [] // The actual events will be generated by the workflow + }); + } catch (error) { + // Log error + logger?.error('Failed to schedule command', { + operation: 'routes/commands.ts:postCommand', + commandId: command.id, + commandType: command.type, + tenantId: command.tenant_id, + error + }); + + // Return error response + res.status(500).json({ + status: 'fail', + error: error instanceof Error ? error.message : String(error) + }); + } finally { + // Close the scheduler + await scheduler.close(); + } + } catch (error) { + logger?.error('Command submission error:', { + operation: 'routes/commands.ts:postCommand', + error + }); + res.status(500).json({ + status: 'fail', + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +export default router; diff --git a/src/core/system/payload-schemas.ts b/src/core/system/payload-schemas.ts index 1a677397..10ced401 100644 --- a/src/core/system/payload-schemas.ts +++ b/src/core/system/payload-schemas.ts @@ -5,16 +5,29 @@ import { SystemCommandType, SystemEventType } from './contracts'; // -- commands export const LogMessagePayloadSchema = z.object({ + aggregateId: z.string(), // todo id column wiring (testId = aggregateId), shouldnt bleed into contracts + aggregateType: z.string(), // todo workflow router - cmd handler should be able to lookup via registry message: z.string(), - systemId: z.string().uuid().optional(), + systemId: z.string().uuid().optional(), // todo singleton ids = validation trouble. see aggregate code for the ='system' hack }); export type LogMessagePayload = z.infer; export const SimulateFailurePayloadSchema = z.object({ + aggregateId: z.string(), + aggregateType: z.string(), systemId: z.string().uuid().optional(), }); export type SimulateFailurePayload = z.infer; +//todo rest are NOT DEVX compatible / reflect reality re aggregateType and Id in payloads. need to decide: +// - before handler, caller figures out aggregate type and prefill +// - options: command-bus, scheduler +// - good: easy wiring +// - bad: implicit / can look like magic +// - Define shared payload +// - good: explicit +// - bad: CMDs can be cross cutting aggregates, although rare and can be handled with override exceptions + export const EmitMultipleEventsPayloadSchema = z.object({ count: z.number(), systemId: z.string().uuid().optional(), diff --git a/src/infra/temporal/temporal-scheduler.ts b/src/infra/temporal/temporal-scheduler.ts index d68740aa..f3765449 100644 --- a/src/infra/temporal/temporal-scheduler.ts +++ b/src/infra/temporal/temporal-scheduler.ts @@ -63,6 +63,7 @@ export class TemporalScheduler implements JobSchedulerPort, EventPublisherPort { logger?.error('Failed to schedule command', { error: e }); } } else { + // @ts-ignore logger?.warn('No router supports command'); } } From 3c42353d4247fbf07166a45763b325ce7ae28d98 Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Mon, 9 Jun 2025 22:37:56 -0700 Subject: [PATCH 08/21] feat(core): add aggregate routing support and lint tool src/tools/lint-payloads/index.ts: Introduced a lint tool to detect missing `aggregateRouting` in registered commands. src/core/system/register.ts: Added `aggregateRouting` metadata to system commands for consistent aggregate handling. src/api/routes/commands.ts: Updated scheduling response to return router results. src/core/registry.ts: Extended `CommandTypeMeta` with `aggregateRouting` for routing declarative metadata. src/infra/temporal/workflow-router.ts: Integrated `aggregateRouting` logic into command handling for workflows. .github/workflows/core-linter.yml: Added GitHub action for `core-lint` validation in PRs. src/core/system/payload-schemas.ts: Removed redundant fields (`aggregateId`, `aggregateType`) from payload schemas. README.md: Added status badge for `core-linter.yml`. devex-ui/src/components/Header.tsx: Updated roles list to reflect new role categories. --- .github/workflows/core-linter.yml | 26 ++++++++++ ADRs/021-core-aggregate-routing.md | 63 ++++++++++++++++++++++++ README.md | 1 + devex-ui/src/components/Header.tsx | 2 +- package.json | 1 + src/api/middlewares/accessLog.ts | 18 +++---- src/api/routes/commands.ts | 7 +-- src/core/ports.ts | 3 +- src/core/registry.ts | 6 ++- src/core/system/payload-schemas.ts | 3 -- src/core/system/register.ts | 8 ++- src/infra/temporal/temporal-scheduler.ts | 23 ++++++--- src/infra/temporal/workflow-router.ts | 18 ++++--- src/tools/lint-payloads/index.ts | 36 ++++++++++++++ 14 files changed, 180 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/core-linter.yml create mode 100644 ADRs/021-core-aggregate-routing.md create mode 100755 src/tools/lint-payloads/index.ts diff --git a/.github/workflows/core-linter.yml b/.github/workflows/core-linter.yml new file mode 100644 index 00000000..7eecea83 --- /dev/null +++ b/.github/workflows/core-linter.yml @@ -0,0 +1,26 @@ +name: Core Linter + +on: + pull_request: + branches: + - '**' + +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: '22' + cache: 'npm' + + - uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - run: npm ci + - run: npm run tool:core-lint diff --git a/ADRs/021-core-aggregate-routing.md b/ADRs/021-core-aggregate-routing.md new file mode 100644 index 00000000..f4cf1972 --- /dev/null +++ b/ADRs/021-core-aggregate-routing.md @@ -0,0 +1,63 @@ +# ADR 021: Co-locate Aggregate Routing with Command Registration + +## Context + +The system currently registers command types separately from their routing logic. Routing to aggregates (i.e., deriving `aggregateType` and `aggregateId` from a command) is either inferred or redundantly handled elsewhere. This introduces implicit coupling and runtime ambiguity, especially for commands triggered via sagas or external workflows. + +In particular: + +* Aggregates rely on `aggregateId` and `aggregateType` being available in payloads. +* Command metadata lives in `DomainRegistry.commandTypes()` but lacks routing. +* Workflows (`processCommand`) must inject routing metadata for proper aggregate reconstruction. +* Ambiguous `aggregateId` primary key mappings + +This violates the principle of co-location and increases the risk of drift between command schemas and routing logic. + +## Decision + +Extend `CommandTypeMeta` to include an optional `aggregateRouting` key: + +```ts +aggregateRouting?: { + aggregateType: string; + extractId: (payload: any) => UUID; +}; +``` + +Update all `registerCommandType()` calls (e.g., in `system/register.ts`) to include routing metadata where applicable. + +During workflow dispatch in `WorkflowRouter.handle(cmd)`, inject routing data into command payloads before processing: + +```ts +const meta = DomainRegistry.commandTypes()[cmd.type]; +const routing = meta?.aggregateRouting; + +if (routing) { + cmd.payload.aggregateType ??= routing.aggregateType; + cmd.payload.aggregateId ??= routing.extractId(cmd.payload); +} +``` + +## Consequences + +### Pros + +* Declarative routing co-located with command type definition +* Avoids ad-hoc inference of `aggregateType` / `aggregateId` at runtime +* Enables automated analysis (e.g., lint-payloads tool) to verify routing completeness +* Respects open/closed principle — saga/workflow code need not change per command type + +### Cons + +* Requires updating all `registerCommandType()` calls with routing +* Minor learning curve: developers must now specify `aggregateRouting` explicitly + +## Alternatives Considered + +* **Central Routing Table**: Rejected — adds indirection, risks desync, harder to statically analyze. +* **Assume client prepopulates routing**: Rejected — contradicts encapsulation; workflows should enforce context, not leak it. + +## Implementation Notes + +* A lint tool (`src/tools/lint-payloads.ts`) will enforce that all registered command types with `payloadSchema` also declare `aggregateRouting` (unless explicitly marked as saga-only). +* This ADR aligns with system goals of deterministic routing, explicit boundaries, and tooling-aware metadata propagation. diff --git a/README.md b/README.md index 958f9ad0..dc0f3b9e 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![CI](https://github.com/geeewhy/intent/actions/workflows/build.yml/badge.svg)](https://github.com/geeewhy/intent/actions/workflows/build.yml) [![CI](https://github.com/geeewhy/intent/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/geeewhy/intent/actions/workflows/unit-tests.yml) +[![CI](https://github.com/geeewhy/intent/actions/workflows/core-linter.yml/badge.svg)](https://github.com/geeewhy/intent/actions/workflows/core-linter.yml) [![CI](https://github.com/geeewhy/intent/actions/workflows/projection-linter.yml/badge.svg)](https://github.com/geeewhy/intent/actions/workflows/projection-linter.yml) > **Intent** turns event-sourcing theory into a platform you can demo in five minutes. It’s a pragmatic, ports-first reference for multi-tenant, event-sourced CQRS back-ends powered by TypeScript and uses [Temporal](https://github.com/temporalio/temporal) for durable workflow execution. diff --git a/devex-ui/src/components/Header.tsx b/devex-ui/src/components/Header.tsx index 1c7fa5cf..3f792508 100644 --- a/devex-ui/src/components/Header.tsx +++ b/devex-ui/src/components/Header.tsx @@ -14,7 +14,7 @@ import { isMock, apiMode } from '@/config/apiMode'; import { Logo } from './Logo'; const tenants = ['tenant-1', 'tenant-2', 'tenant-3', '0af03580-98d5-4884-96e4-e75168d8b887']; -const roles = ['admin', 'user', 'viewer']; +const roles = ['admin', 'developer', 'tester']; export const Header = () => { const { tenant, role, setTenant, setRole } = useAppCtx(); diff --git a/package.json b/package.json index 96369b63..08d46e48 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "tool:projections-lint": "ts-node src/tools/projections-lint/index.ts", "tool:projection-check-drift": "ts-node src/tools/projections-drift-check/index.ts", "tool:projection-repair": "ts-node src/tools/projections-repair/index.ts", + "tool:core-lint": "ts-node src/tools/core-lint/index.ts", "setup": "ts-node src/tools/setup/setup.ts" }, "dependencies": { diff --git a/src/api/middlewares/accessLog.ts b/src/api/middlewares/accessLog.ts index 320e27e6..e5667abd 100644 --- a/src/api/middlewares/accessLog.ts +++ b/src/api/middlewares/accessLog.ts @@ -11,15 +11,15 @@ export const accessLogMiddleware = (req: Request, res: Response, next: NextFunct // Log request details when the response is finished res.on('finish', () => { const duration = Date.now() - startTime; - stdLogger.info('API Request', { - operation: 'middlewares/accessLog.ts:accessLogMiddleware', - method: req.method, - path: req.originalUrl || req.url, - statusCode: res.statusCode, - duration: `${duration}ms`, - ip: req.ip || req.socket.remoteAddress, - userAgent: req.get('User-Agent') - }); + // stdLogger.info('API Request', { + // operation: 'middlewares/accessLog.ts:accessLogMiddleware', + // method: req.method, + // path: req.originalUrl || req.url, + // statusCode: res.statusCode, + // duration: `${duration}ms`, + // ip: req.ip || req.socket.remoteAddress, + // userAgent: req.get('User-Agent') + // }); }); next(); diff --git a/src/api/routes/commands.ts b/src/api/routes/commands.ts index a054ffb8..1902cae4 100644 --- a/src/api/routes/commands.ts +++ b/src/api/routes/commands.ts @@ -94,7 +94,7 @@ router.post('/api/commands', async (req, res) => { try { // Schedule the command console.log(command); - await scheduler.schedule(command); + const schedulerResponse = await scheduler.schedule(command); // Log success logger?.info('Command scheduled successfully', { @@ -105,10 +105,7 @@ router.post('/api/commands', async (req, res) => { }); // Return success response - res.json({ - status: 'success', - events: [] // The actual events will be generated by the workflow - }); + res.json(schedulerResponse); } catch (error) { // Log error logger?.error('Failed to schedule command', { diff --git a/src/core/ports.ts b/src/core/ports.ts index 78466823..a152665a 100644 --- a/src/core/ports.ts +++ b/src/core/ports.ts @@ -4,6 +4,7 @@ */ import { Command, Event, UUID } from './contracts'; +import {CommandResult} from "../infra/contracts"; /** * Inbound port for handling commands @@ -58,7 +59,7 @@ export interface EventStorePort { * Outbound port for scheduling jobs/workflows */ export interface JobSchedulerPort { - schedule(cmd: Command): Promise; + schedule(cmd: Command): Promise; } /** diff --git a/src/core/registry.ts b/src/core/registry.ts index 26cd32b8..256c9ac9 100644 --- a/src/core/registry.ts +++ b/src/core/registry.ts @@ -1,5 +1,5 @@ //src/core/registry.ts -import { CommandHandler, EventHandler, SagaDefinition, ReadModelUpdaterPort } from './contracts'; +import { CommandHandler, EventHandler, SagaDefinition, ReadModelUpdaterPort, UUID } from './contracts'; import { AggregateClass } from './aggregates'; import { z } from 'zod'; @@ -8,6 +8,10 @@ export interface CommandTypeMeta { domain: string; description: string; payloadSchema?: z.ZodTypeAny; + aggregateRouting?: { + aggregateType: string; + extractId: (payload: any) => UUID; + }; } export interface EventTypeMeta { diff --git a/src/core/system/payload-schemas.ts b/src/core/system/payload-schemas.ts index 10ced401..0e9ea61d 100644 --- a/src/core/system/payload-schemas.ts +++ b/src/core/system/payload-schemas.ts @@ -5,8 +5,6 @@ import { SystemCommandType, SystemEventType } from './contracts'; // -- commands export const LogMessagePayloadSchema = z.object({ - aggregateId: z.string(), // todo id column wiring (testId = aggregateId), shouldnt bleed into contracts - aggregateType: z.string(), // todo workflow router - cmd handler should be able to lookup via registry message: z.string(), systemId: z.string().uuid().optional(), // todo singleton ids = validation trouble. see aggregate code for the ='system' hack }); @@ -14,7 +12,6 @@ export type LogMessagePayload = z.infer; export const SimulateFailurePayloadSchema = z.object({ aggregateId: z.string(), - aggregateType: z.string(), systemId: z.string().uuid().optional(), }); export type SimulateFailurePayload = z.infer; diff --git a/src/core/system/register.ts b/src/core/system/register.ts index bc98d3a3..4942db5f 100644 --- a/src/core/system/register.ts +++ b/src/core/system/register.ts @@ -14,6 +14,11 @@ import { SystemCommandType, SystemEventType } from './contracts'; import { register as registerSystemProjections } from './read-models/register'; import { commandPayloadSchemas, eventPayloadSchemas } from './payload-schemas'; +const aggregateRouting = { + aggregateType: 'system', + extractId: (payload: Partial<{ systemId: any; }>) => payload.systemId || 'system' +} + /** * Self-registration function for the system domain * Registers all system domain components with the central registry @@ -41,7 +46,8 @@ export function registerSystemDomain(): void { registerCommandType(type, { domain: 'system', description: `System command: ${type}`, - payloadSchema: commandPayloadSchemas[type] + payloadSchema: commandPayloadSchemas[type], + aggregateRouting: aggregateRouting }); }); diff --git a/src/infra/temporal/temporal-scheduler.ts b/src/infra/temporal/temporal-scheduler.ts index f3765449..7030ff31 100644 --- a/src/infra/temporal/temporal-scheduler.ts +++ b/src/infra/temporal/temporal-scheduler.ts @@ -16,7 +16,7 @@ export class TemporalScheduler implements JobSchedulerPort, EventPublisherPort { private constructor( private readonly router: WorkflowRouter, private readonly client: WorkflowClient, - private readonly commandStore:CommandStorePort + private readonly commandStore: CommandStorePort ) { } @@ -41,7 +41,7 @@ export class TemporalScheduler implements JobSchedulerPort, EventPublisherPort { /** * Schedule a command for execution via Temporal */ - async schedule(cmd: Command): Promise { + async schedule(cmd: Command): Promise { const logger = log()?.child({ commandId: cmd.id, commandType: cmd.type, @@ -49,23 +49,30 @@ export class TemporalScheduler implements JobSchedulerPort, EventPublisherPort { correlationId: cmd.metadata?.correlationId }); - logger?.info('Routing command'); + let res: CommandResult = { + status: 'fail', + error: 'Command not supported' + }; + + logger?.info('Routing command', {cmd}); if (this.router.supportsCommand(cmd)) { try { await this.commandStore.upsert(cmd); - const res: CommandResult = await this.router.handle(cmd); + res = await this.router.handle(cmd); const infraStatus = res.status === 'success' ? 'consumed' : 'failed'; await this.commandStore.markStatus(cmd.id, infraStatus, res); - logger?.info('Marked command status', { status: infraStatus }); + logger?.debug('Marked command status', {status: infraStatus}); } catch (e: any) { - await this.commandStore.markStatus(cmd.id, 'failed', { status: 'fail', error: e.message }); - logger?.error('Failed to schedule command', { error: e }); + res = {status: 'fail', error: e.message}; + await this.commandStore.markStatus(cmd.id, 'failed', res); + logger?.error('Failed to schedule command', {error: e}); } } else { - // @ts-ignore logger?.warn('No router supports command'); } + + return res; } /** diff --git a/src/infra/temporal/workflow-router.ts b/src/infra/temporal/workflow-router.ts index 048c99c3..bbb9a802 100644 --- a/src/infra/temporal/workflow-router.ts +++ b/src/infra/temporal/workflow-router.ts @@ -1,6 +1,6 @@ // infra/temporal/workflow-router.ts import {Connection, WorkflowClient, WorkflowIdReusePolicy} from '@temporalio/client'; -import {getAllSagas} from '../../core/registry'; +import {getAllSagas, DomainRegistry} from '../../core/registry'; import {Command, Event, UUID} from '../../core/contracts'; import {log} from '../../core/logger'; import {CommandHandler} from '../../core/contracts'; @@ -28,10 +28,7 @@ export class WorkflowRouter implements CommandHandler, EventHandler { /** Supports command routing (aggregate or saga) */ supportsCommand(cmd: Command): boolean { - return ( - (!!cmd.payload?.aggregateId && !!cmd.payload?.aggregateType) || - Object.values(SagaRegistry).some((s) => s.idFor(cmd)) - ); + return this.isAggregateCommand(cmd) || this.isSagaCommand(cmd); } /** Supports event routing */ @@ -41,6 +38,14 @@ export class WorkflowRouter implements CommandHandler, EventHandler { /** Handle a command (always route to aggregate's processCommand workflow) */ async handle(cmd: Command): Promise { + const meta = DomainRegistry.commandTypes()[cmd.type]; + const routing = meta?.aggregateRouting; + + if (routing) { + cmd.payload.aggregateType ??= routing.aggregateType; + cmd.payload.aggregateId ??= routing.extractId(cmd.payload); + } + if (this.isAggregateCommand(cmd)) { const {tenant_id} = cmd; const aggregateType = cmd.payload?.aggregateType; @@ -233,7 +238,8 @@ export class WorkflowRouter implements CommandHandler, EventHandler { /** Check if a command is for an aggregate */ private isAggregateCommand(cmd: Command): boolean { - return !!cmd.payload?.aggregateId && !!cmd.payload?.aggregateType; + const meta = DomainRegistry.commandTypes()[cmd.type]; + return !!meta?.aggregateRouting || (!!cmd.payload?.aggregateId && !!cmd.payload?.aggregateType); } /** Get workflow ID for aggregates */ diff --git a/src/tools/lint-payloads/index.ts b/src/tools/lint-payloads/index.ts new file mode 100755 index 00000000..e25e3965 --- /dev/null +++ b/src/tools/lint-payloads/index.ts @@ -0,0 +1,36 @@ +#!/usr/bin/env node +/** + * Lint-core tool + * + * Placeholder. + * Currently, checks that all registered commands with a payloadSchema also declare aggregateRouting (if not saga-only) + */ + +import { DomainRegistry } from '../../core/registry'; + +// Get all registered command types +const commandTypes = DomainRegistry.commandTypes(); + +// Track issues +const issues: string[] = []; + +// Check each command type +Object.entries(commandTypes).forEach(([type, meta]) => { + // Skip commands without a payload schema + if (!meta.payloadSchema) return; + + // Check if the command has aggregate routing + if (!meta.aggregateRouting) { + issues.push(`Command type '${type}' has a payloadSchema but no aggregateRouting`); + } +}); + +// Report results +if (issues.length > 0) { + console.error('Payload linting found issues:'); + issues.forEach(issue => console.error(`- ${issue}`)); + process.exit(1); +} else { + console.log('All command payloads have proper routing configuration.'); + process.exit(0); +} \ No newline at end of file From 27d06ca9d0eea645cae423d85a18c50ab06ad28f Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Mon, 9 Jun 2025 23:51:02 -0700 Subject: [PATCH 09/21] feat(ui): add role switch, enhance cmd highlighting src/components/Header.tsx: Removed role switcher; simplified tenant UI. src/components/CommandIssuer.tsx: Replaced aggregate ID with role switcher. Added cmd highlight for recent submissions, adjusted toast UX. --- devex-ui/src/App.css | 1 + devex-ui/src/components/CommandIssuer.tsx | 242 +++++++++++----------- devex-ui/src/components/Header.tsx | 27 +-- 3 files changed, 128 insertions(+), 142 deletions(-) diff --git a/devex-ui/src/App.css b/devex-ui/src/App.css index b9d355df..da788834 100644 --- a/devex-ui/src/App.css +++ b/devex-ui/src/App.css @@ -40,3 +40,4 @@ .read-the-docs { color: #888; } + diff --git a/devex-ui/src/components/CommandIssuer.tsx b/devex-ui/src/components/CommandIssuer.tsx index a82cb065..e39128a1 100644 --- a/devex-ui/src/components/CommandIssuer.tsx +++ b/devex-ui/src/components/CommandIssuer.tsx @@ -9,7 +9,6 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Clock, Send, RotateCcw, Terminal, ChevronDown, ChevronRight, RefreshCw, AlertCircle } from "lucide-react"; -import type { CommandSchema } from "@/data/types"; import { useCommands, useSubmitCommand } from "@/hooks/api"; import { validate, registerSchemas } from "@/utils/schemaValidator"; import { makeExample } from "@/utils/schemaFaker"; @@ -17,21 +16,21 @@ import { toast } from "@/components/ui/sonner"; import { useAppCtx } from '@/app/AppProvider'; import { useQuery } from "@tanstack/react-query"; import { fetchCommandRegistry } from "@/data/apiService"; +import { cn } from "@/lib/utils"; -const generateUUID4 = (): string => { - return crypto.randomUUID(); -}; +const roles = ['admin', 'developer', 'tester', 'johndoe']; export const CommandIssuer = () => { - const { tenant } = useAppCtx(); + const { tenant, role, setRole } = useAppCtx(); const [selectedCommand, setSelectedCommand] = useState(""); - const [aggregateId, setAggregateId] = useState(""); const [payload, setPayload] = useState(""); const [formData, setFormData] = useState>({}); const [payloadView, setPayloadView] = useState<"form" | "json">("form"); const [expandedCommand, setExpandedCommand] = useState(null); const [validationErrors, setValidationErrors] = useState([]); const [invalidFields, setInvalidFields] = useState>(new Set()); + const [lastSubmittedId, setLastSubmittedId] = useState(null); + const [highlightedCommandId, setHighlightedCommandId] = useState(null); // Use React Query hooks const { data: recentCommands = [] } = useCommands(tenant, 10); @@ -115,15 +114,12 @@ export const CommandIssuer = () => { id: crypto.randomUUID(), tenant_id: tenant, type: selectedCommand, - payload: { - ...payloadData, - ...(aggregateId ? { aggregateId } : {}) - }, + payload: payloadData, metadata: { timestamp: new Date().toISOString(), userId: crypto.randomUUID(), - role: 'user', - source: 'command-issuer' + role, + source: 'devx/command-issuer' } }; @@ -133,25 +129,31 @@ export const CommandIssuer = () => { onSuccess: (result) => { console.log('Command submission result:', result); - toast.success( - result.status === 'success' - ? 'Command executed 🎉' - : 'Command failed', - { - description: - result.status === 'success' - ? `${result.events?.length ?? 0} event(s) produced` - : result.error - } - ); - - // Reset form - setSelectedCommand(""); - setAggregateId(""); - setPayload(""); - setFormData({}); - setValidationErrors([]); - setInvalidFields(new Set()); + if (result.status === 'success') { + toast.success('Command executed', { + description: `${result.events?.length > 0 ? result.events?.length + 'event(s) produced' : ''}` + }); + + // Set the last submitted ID + setLastSubmittedId(commandPayload.id); + setHighlightedCommandId(commandPayload.id); + + setTimeout(() => { + setHighlightedCommandId(null); + }, 500); + + // Reset form + setPayload(""); + setFormData({}); + setValidationErrors([]); + setInvalidFields(new Set()); + } else { + toast.error('Command failed', { + description: 'Error:' + result.error || 'Unknown failure' + }); + + // Keep form open (do not reset anything) + } }, onError: (error) => { console.error('Command submission failed:', error); @@ -164,9 +166,6 @@ export const CommandIssuer = () => { }); }; - const generateAggregateId = () => { - setAggregateId(generateUUID4()); - }; const handleFormDataChange = (key: string, value: unknown) => { @@ -342,25 +341,19 @@ export const CommandIssuer = () => {
- -
- setAggregateId(e.target.value)} - placeholder="e.g., system-123" - className="bg-slate-800 border-slate-700 text-slate-100" - /> - -
+ +
@@ -474,75 +467,88 @@ export const CommandIssuer = () => {
- {recentCommands.map((cmd) => ( -
-
toggleCommandExpansion(cmd.id)} + {recentCommands.map((cmd, idx) => { + const isHighlighted = cmd.id === highlightedCommandId; + const highlightClass = isHighlighted ? "ring-2 ring-blue-500 bg-blue-500/10" : ""; + return ( +
-
- {expandedCommand === cmd.id ? ( - - ) : ( - - )} - { - cmd.createdAt && - {new Date(cmd.createdAt).toLocaleString(undefined, { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} - - } - {cmd.id} - {cmd.type} -
- - toggleCommandExpansion(cmd.id)} > - {cmd.status} - -
- - {expandedCommand === cmd.id && ( -
- - {new Date(cmd.createdAt).toLocaleString(undefined, { - year: '2-digit', - month: '2-digit', - day: '2-digit', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - })} - -
-
Payload:
-
-                          {JSON.stringify(cmd.payload, null, 2)}
-                        
-
-
-
Response:
-
-                          {JSON.stringify(cmd.response, null, 2)}
-                        
+
+ {expandedCommand === cmd.id ? ( + + ) : ( + + )} + { + cmd.createdAt && + {new Date(cmd.createdAt).toLocaleString(undefined, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} + + } + {cmd.id} + {cmd.type}
+ + + {cmd.status} +
- )} -
- ))} + + {expandedCommand === cmd.id && ( +
+ + {new Date(cmd.createdAt).toLocaleString(undefined, { + year: '2-digit', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + })} + +
+
Metadata:
+
+                            {JSON.stringify(cmd.metadata ?? {}, null, 2)}
+                          
+
+
+
Payload:
+
+                            {JSON.stringify(cmd.payload, null, 2)}
+                          
+
+
+
Response:
+
+                            {JSON.stringify(cmd.response, null, 2)}
+                          
+
+
+ )} +
+ ); + })}
diff --git a/devex-ui/src/components/Header.tsx b/devex-ui/src/components/Header.tsx index 3f792508..a1f722c9 100644 --- a/devex-ui/src/components/Header.tsx +++ b/devex-ui/src/components/Header.tsx @@ -13,11 +13,10 @@ import { useAppCtx } from '@/app/AppProvider'; import { isMock, apiMode } from '@/config/apiMode'; import { Logo } from './Logo'; -const tenants = ['tenant-1', 'tenant-2', 'tenant-3', '0af03580-98d5-4884-96e4-e75168d8b887']; -const roles = ['admin', 'developer', 'tester']; +const tenants = ['0af03580-98d5-4884-96e4-e75168d8b887']; export const Header = () => { - const { tenant, role, setTenant, setRole } = useAppCtx(); + const { tenant, setTenant } = useAppCtx(); return (
@@ -27,7 +26,7 @@ export const Header = () => { Intent DevX
- {/* Tenant/Role Switchers */} + {/* Tenant switcher */}
@@ -48,26 +47,6 @@ export const Header = () => { ))} - - - - - - - {roles.map((role) => ( - setRole(role)} - className="text-slate-100 hover:bg-slate-700" - > - {role} - - ))} - -
{/* Spacer */} From 342d72e1fa54752833cd52be400894d59f19acb0 Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 01:00:23 -0700 Subject: [PATCH 10/21] feat(logger): integrate apiLogger into middleware and routes src/infra/logger/apiLogger.ts, src/api/logger.ts: Added and exported `apiLogger` for modular logging focused on API layer encapsulation. src/api/middlewares/accessLog.ts: Switched to `apiLogger` for request/response logging, replacing `stdLogger` for enhanced modularity. src/api/routes/commands.ts: Replaced `stdLogger` with `logger` from `apiLogger`, removed redundant logger setup for improved consistency and cleaner dependency management. --- src/api/logger.ts | 6 ++++++ src/api/middlewares/accessLog.ts | 22 +++++++++++----------- src/api/routes/commands.ts | 16 ++++++++-------- src/infra/logger/apiLogger.ts | 7 +++++++ 4 files changed, 32 insertions(+), 19 deletions(-) create mode 100644 src/api/logger.ts create mode 100644 src/infra/logger/apiLogger.ts diff --git a/src/api/logger.ts b/src/api/logger.ts new file mode 100644 index 00000000..af66c912 --- /dev/null +++ b/src/api/logger.ts @@ -0,0 +1,6 @@ +import { apiLogger } from '../infra/logger/apiLogger'; +import { setLoggerAccessor } from '../core/logger'; + +setLoggerAccessor(() => apiLogger); + +export default apiLogger; \ No newline at end of file diff --git a/src/api/middlewares/accessLog.ts b/src/api/middlewares/accessLog.ts index e5667abd..ab1fca55 100644 --- a/src/api/middlewares/accessLog.ts +++ b/src/api/middlewares/accessLog.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from 'express'; -import { stdLogger } from '../../infra/logger/stdLogger'; +import { apiLogger } from '../../infra/logger/apiLogger'; /** * Access logging middleware @@ -11,18 +11,18 @@ export const accessLogMiddleware = (req: Request, res: Response, next: NextFunct // Log request details when the response is finished res.on('finish', () => { const duration = Date.now() - startTime; - // stdLogger.info('API Request', { - // operation: 'middlewares/accessLog.ts:accessLogMiddleware', - // method: req.method, - // path: req.originalUrl || req.url, - // statusCode: res.statusCode, - // duration: `${duration}ms`, - // ip: req.ip || req.socket.remoteAddress, - // userAgent: req.get('User-Agent') - // }); + apiLogger.info('Access log', { + operation: 'accessLogMiddleware', + method: req.method, + path: req.originalUrl || req.url, + statusCode: res.statusCode, + duration: duration, + ip: req.ip || req.socket.remoteAddress, + userAgent: req.get('User-Agent') + }); }); next(); }; -export default accessLogMiddleware; \ No newline at end of file +export default accessLogMiddleware; diff --git a/src/api/routes/commands.ts b/src/api/routes/commands.ts index 1902cae4..305c0098 100644 --- a/src/api/routes/commands.ts +++ b/src/api/routes/commands.ts @@ -1,12 +1,7 @@ import { Router } from 'express'; import pool from '../db'; -import {setLoggerAccessor, log} from '../../core/logger'; -import {stdLogger} from '../../infra/logger/stdLogger'; import { TemporalScheduler } from '../../infra/temporal/temporal-scheduler'; - - -setLoggerAccessor(() => stdLogger); -const logger = log(); +import logger from '../logger'; const router = Router(); @@ -40,7 +35,7 @@ router.get('/api/commands', async (req, res) => { client.release(); } } catch (error) { - stdLogger.error('Commands endpoint error:', { error }); + logger.error('Commands endpoint error:', { error }); res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); @@ -104,7 +99,12 @@ router.post('/api/commands', async (req, res) => { tenantId: command.tenant_id }); - // Return success response + if (schedulerResponse.status === 'fail') { + //res.status(422); + } + else { + res.status(200); + } res.json(schedulerResponse); } catch (error) { // Log error diff --git a/src/infra/logger/apiLogger.ts b/src/infra/logger/apiLogger.ts new file mode 100644 index 00000000..5dd31305 --- /dev/null +++ b/src/infra/logger/apiLogger.ts @@ -0,0 +1,7 @@ +import { stdLogger } from './stdLogger'; +import { LoggerPort } from '../../core/ports'; + +export const apiLogger: LoggerPort = stdLogger.child({ + module: 'api', + layer: 'access', +}); \ No newline at end of file From e221c8db272a801cdf36ca06c0817830bbcf749d Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 01:40:19 -0700 Subject: [PATCH 11/21] feat(core): add roles registration and API endpoints src/core/system/register.ts: Introduced `registerRoles` interface and registered domain roles for "system". src/api/routes/registry.ts: Added `/api/registry/roles` endpoint to expose roles meta; updated registry response to include roles data. src/core/registry.ts: Added roles storage, registration, and retrieval utilities in the `DomainRegistry`. src/tools/core-lint/index.ts: Clarified file path for better script organization. --- src/api/routes/registry.ts | 21 ++++++++++++++++++- src/core/registry.ts | 12 +++++++++++ src/core/system/register.ts | 6 +++++- .../{lint-payloads => core-lint}/index.ts | 2 ++ 4 files changed, 39 insertions(+), 2 deletions(-) rename src/tools/{lint-payloads => core-lint}/index.ts (96%) diff --git a/src/api/routes/registry.ts b/src/api/routes/registry.ts index b2cde6e7..e35735f3 100644 --- a/src/api/routes/registry.ts +++ b/src/api/routes/registry.ts @@ -32,6 +32,7 @@ router.get('/api/registry', (req, res) => { sagas: Object.keys(DomainRegistry.sagas()), commandTypes: commands, eventTypes: events, + roles: DomainRegistry.roles(), }); }); @@ -48,6 +49,12 @@ router.get('/api/registry/commands', (req, res) => { res.json(attachSchema(data, includeSchema)); }); +// Roles endpoint +router.get('/api/registry/roles', (req, res) => { + res.json(DomainRegistry.roles()); +}); + + // Events endpoint router.get('/api/registry/events', (req, res) => { const includeSchema = req.query.includeSchema === 'true'; @@ -76,4 +83,16 @@ router.get('/api/registry/domains', (_req, res) => res.json(DomainRegistry.domains()), ); -export default router; \ No newline at end of file +// Roles endpoint +router.get('/api/registry/roles', (req, res) => { + const domain = req.query.domain as string | undefined; + + if (domain) { + const roles = DomainRegistry.roles()[domain] || []; + res.json(roles); + } else { + res.json(DomainRegistry.roles()); + } +}); + +export default router; diff --git a/src/core/registry.ts b/src/core/registry.ts index 256c9ac9..83ca9c6e 100644 --- a/src/core/registry.ts +++ b/src/core/registry.ts @@ -44,6 +44,7 @@ export interface Registry { eventTypes: Record; projections: Record; domains: string[]; + roles: Record; } const registry: Registry = { @@ -55,6 +56,7 @@ const registry: Registry = { eventTypes: {}, projections: {}, domains: [], + roles: {}, }; // --- Registration helpers @@ -103,6 +105,11 @@ export function registerProjection(name: string, def: ProjectionDefinition): voi registry.projections[name] = def; } +export function registerRoles(domain: string, roles: string[]): void { + if (registry.roles[domain]) throw new Error(`Roles already registered for domain: ${domain}`); + registry.roles[domain] = roles; +} + /* ——— Getters ——— */ export function getAllAggregates(): Record { return registry.aggregates; @@ -136,6 +143,10 @@ export function getAllDomains(): string[] { return registry.domains; } +export function getAllRoles(): Record { + return registry.roles; +} + // -- convenience exports export const DomainRegistry = { aggregates: getAllAggregates, @@ -146,6 +157,7 @@ export const DomainRegistry = { eventTypes: getAllEventTypes, projections: getAllProjections, domains: getAllDomains, + roles: getAllRoles, }; export default DomainRegistry; diff --git a/src/core/system/register.ts b/src/core/system/register.ts index 4942db5f..52de797d 100644 --- a/src/core/system/register.ts +++ b/src/core/system/register.ts @@ -6,11 +6,12 @@ import { registerSaga, registerCommandType, registerEventType, + registerRoles, } from '../registry'; import { SystemAggregate } from './aggregates/system.aggregate'; import { SystemCommandHandler } from './command-handler'; import { systemSagaRegistry } from './sagas/saga-registry'; -import { SystemCommandType, SystemEventType } from './contracts'; +import { SystemCommandType, SystemEventType, SystemRole } from './contracts'; import { register as registerSystemProjections } from './read-models/register'; import { commandPayloadSchemas, eventPayloadSchemas } from './payload-schemas'; @@ -59,6 +60,9 @@ export function registerSystemDomain(): void { payloadSchema: eventPayloadSchemas[type] }); }); + + // Register roles + registerRoles('system', ['tester', 'system', 'developer']); } // Auto-register when imported diff --git a/src/tools/lint-payloads/index.ts b/src/tools/core-lint/index.ts similarity index 96% rename from src/tools/lint-payloads/index.ts rename to src/tools/core-lint/index.ts index e25e3965..1dbd6bfc 100755 --- a/src/tools/lint-payloads/index.ts +++ b/src/tools/core-lint/index.ts @@ -1,4 +1,6 @@ #!/usr/bin/env node + +//src/tools/core-lint/index.ts /** * Lint-core tool * From c5955f4ebe2b40ef46e49b3201e0414bb01b0139 Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 02:26:42 -0700 Subject: [PATCH 12/21] feat(roles): add roles management and domain-based query devex-ui/src/mocks/stores/roles.store.ts: Created roles store and helper to manage domain-based roles. devex-ui/src/hooks/api/useRoles.ts: Added a react-query hook to fetch roles by domain. devex-ui/src/mocks/scenarios/default.ts: Seeded roles store with default domain-role mappings for mock data. devex-ui/src/data/apiService.ts: Created fetch function for fetching roles by domain from API. devex-ui/src/mocks/handlers.ts: Added API mock handler for fetching roles, including default fallback. devex-ui/src/hooks/api/queryKeys.ts: Added query keys for roles for use in react-query. devex-ui/src/hooks/api/index.ts: Exported `useRoles` for centralized API hook import. devex-ui/src/mocks/stores/index.ts: Exported `roles.store` for centralized store registry. devex-ui/src/data/api.ts: Refactored API baseUrl to account for `apiMode` changes. devex-ui/src/components/CommandIssuer.tsx: Integrated roles query to dynamically fetch roles based on selected command's domain. devex-ui/src/components/Header.tsx: Minor update to tenants array for example clarity. --- devex-ui/src/components/CommandIssuer.tsx | 6 ++-- devex-ui/src/components/Header.tsx | 2 +- devex-ui/src/data/api.ts | 7 ++-- devex-ui/src/data/apiService.ts | 5 +++ devex-ui/src/hooks/api/index.ts | 1 + devex-ui/src/hooks/api/queryKeys.ts | 6 ++++ devex-ui/src/hooks/api/useRoles.ts | 20 ++++++++++++ devex-ui/src/mocks/handlers.ts | 26 +++++++++++++-- devex-ui/src/mocks/scenarios/default.ts | 40 +++++++++++++++++++++++ devex-ui/src/mocks/stores/index.ts | 1 + devex-ui/src/mocks/stores/roles.store.ts | 19 +++++++++++ 11 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 devex-ui/src/hooks/api/useRoles.ts create mode 100644 devex-ui/src/mocks/stores/roles.store.ts diff --git a/devex-ui/src/components/CommandIssuer.tsx b/devex-ui/src/components/CommandIssuer.tsx index e39128a1..a8ce494f 100644 --- a/devex-ui/src/components/CommandIssuer.tsx +++ b/devex-ui/src/components/CommandIssuer.tsx @@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Clock, Send, RotateCcw, Terminal, ChevronDown, ChevronRight, RefreshCw, AlertCircle } from "lucide-react"; -import { useCommands, useSubmitCommand } from "@/hooks/api"; +import { useCommands, useSubmitCommand, useRoles } from "@/hooks/api"; import { validate, registerSchemas } from "@/utils/schemaValidator"; import { makeExample } from "@/utils/schemaFaker"; import { toast } from "@/components/ui/sonner"; @@ -18,8 +18,6 @@ import { useQuery } from "@tanstack/react-query"; import { fetchCommandRegistry } from "@/data/apiService"; import { cn } from "@/lib/utils"; -const roles = ['admin', 'developer', 'tester', 'johndoe']; - export const CommandIssuer = () => { const { tenant, role, setRole } = useAppCtx(); const [selectedCommand, setSelectedCommand] = useState(""); @@ -280,6 +278,8 @@ export const CommandIssuer = () => { }; const selectedCommandSchema = commandRegistry.find(cmd => cmd.type === selectedCommand); + const domain = selectedCommandSchema?.domain; + const { data: roles = [] } = useRoles(domain); const toggleCommandExpansion = (commandId: string) => { setExpandedCommand(expandedCommand === commandId ? null : commandId); diff --git a/devex-ui/src/components/Header.tsx b/devex-ui/src/components/Header.tsx index a1f722c9..c1b8f756 100644 --- a/devex-ui/src/components/Header.tsx +++ b/devex-ui/src/components/Header.tsx @@ -13,7 +13,7 @@ import { useAppCtx } from '@/app/AppProvider'; import { isMock, apiMode } from '@/config/apiMode'; import { Logo } from './Logo'; -const tenants = ['0af03580-98d5-4884-96e4-e75168d8b887']; +const tenants = ['tenant-1','0af03580-98d5-4884-96e4-e75168d8b887']; export const Header = () => { const { tenant, setTenant } = useAppCtx(); diff --git a/devex-ui/src/data/api.ts b/devex-ui/src/data/api.ts index cf22873d..98cf40b8 100644 --- a/devex-ui/src/data/api.ts +++ b/devex-ui/src/data/api.ts @@ -1,6 +1,8 @@ //devex-ui/src/data/api.ts import { toast } from "@/components/ui/sonner"; +const apiMode = localStorage.getItem('api_mode') || import.meta.env.VITE_API_MODE || 'mock'; + // API client configuration export const API_CONFIG = { baseUrl: localStorage.getItem('api_uri') || import.meta.env.VITE_API_URL || '', @@ -17,9 +19,8 @@ export const API_CONFIG = { // --- URL builder function buildUrl(endpoint: string, params?: Record): string { - const url = API_CONFIG.baseUrl - ? new URL(`${API_CONFIG.baseUrl}${endpoint}`) - : new URL(endpoint, window.location.origin); + const base = apiMode === 'mock' ? '' : (localStorage.getItem('api_uri') || import.meta.env.VITE_API_URL || ''); + const url = new URL(`${base}${endpoint}`, window.location.origin); if (params) { Object.entries(params).forEach(([k, v]) => url.searchParams.append(k, v)); diff --git a/devex-ui/src/data/apiService.ts b/devex-ui/src/data/apiService.ts index 23d58c62..cf1c94b8 100644 --- a/devex-ui/src/data/apiService.ts +++ b/devex-ui/src/data/apiService.ts @@ -53,3 +53,8 @@ export const fetchLogs = (tenant: string, limit=50) => export const fetchCommandRegistry = async (): Promise => { return apiClient.get(API_CONFIG.endpoints.registry, { includeSchema: 'true' }); }; + +export const fetchRolesByDomain = async (domain: string): Promise => { + const result = await apiClient.get>('/api/registry/roles'); + return result[domain] ?? []; +}; diff --git a/devex-ui/src/hooks/api/index.ts b/devex-ui/src/hooks/api/index.ts index 01f772ae..1a4af924 100644 --- a/devex-ui/src/hooks/api/index.ts +++ b/devex-ui/src/hooks/api/index.ts @@ -5,3 +5,4 @@ export * from './useEvent'; export * from './useCommands'; export * from './useSubmitCommand'; export * from './useMetrics'; +export * from './useRoles'; diff --git a/devex-ui/src/hooks/api/queryKeys.ts b/devex-ui/src/hooks/api/queryKeys.ts index 693f4782..2a67a690 100644 --- a/devex-ui/src/hooks/api/queryKeys.ts +++ b/devex-ui/src/hooks/api/queryKeys.ts @@ -19,3 +19,9 @@ export const logsKeys = { lists: () => [...logsKeys.all, 'list'] as const, list: (tenant: string, limit: number) => [...logsKeys.lists(), tenant, limit] as const, } + +export const rolesKeys = { + all: ['roles'] as const, + lists: () => [...rolesKeys.all, 'list'] as const, + list: (domain: string) => [...rolesKeys.lists(), domain] as const, +} diff --git a/devex-ui/src/hooks/api/useRoles.ts b/devex-ui/src/hooks/api/useRoles.ts new file mode 100644 index 00000000..99676075 --- /dev/null +++ b/devex-ui/src/hooks/api/useRoles.ts @@ -0,0 +1,20 @@ +//devex-ui/src/hooks/api/useRoles.ts +import { useQuery } from '@tanstack/react-query'; +import { isMock } from '@/config/apiMode'; +import { fetchRolesByDomain } from '@/data'; +import { rolesStore } from '@/mocks/stores/roles.store'; +import { rolesKeys } from './queryKeys'; + +export function useRoles(domain: string) { + return useQuery({ + queryKey: rolesKeys.list(domain), + queryFn: async () => { + if (isMock) { + const match = rolesStore.list().find(r => r.domain === domain); + return match?.roles ?? []; + } + return fetchRolesByDomain(domain); + }, + enabled: !!domain + }); +} \ No newline at end of file diff --git a/devex-ui/src/mocks/handlers.ts b/devex-ui/src/mocks/handlers.ts index 0b5aa5d3..f0c4096f 100644 --- a/devex-ui/src/mocks/handlers.ts +++ b/devex-ui/src/mocks/handlers.ts @@ -9,14 +9,15 @@ import { findTracesByCorrelationId, generateEdges, logStore, - pushLog + pushLog, + rolesStore } from './stores'; import { makeEvent } from './factories/event.factory'; import { makeLog } from './factories/log.factory'; import { makeCommandRegistry } from './factories/registry.factory'; export const handlers = [ - // Registry handler + // Registry handlers http.get('/api/registry/commands', ({ request }) => { const url = new URL(request.url); const includeSchema = url.searchParams.get('includeSchema') === 'true'; @@ -26,6 +27,27 @@ export const handlers = [ includeSchema ? registry : registry.map(({ schema, ...rest }) => rest) ); }), + + http.get('/api/registry/roles', ({ request }) => { + // Convert roles store data to the expected format + const rolesData = rolesStore.list(1000); + const result: Record = {}; + + rolesData.forEach(entry => { + result[entry.domain] = entry.roles; + }); + + // If no roles are in the store, return default roles + if (Object.keys(result).length === 0) { + return HttpResponse.json({ + system: ['tester', 'system', 'developer'], + user: ['admin', 'viewer'], + order: ['sales', 'ops'] + }); + } + + return HttpResponse.json(result); + }), // Logs list http.get('/api/logs', ({ request }) => { const url = new URL(request.url); diff --git a/devex-ui/src/mocks/scenarios/default.ts b/devex-ui/src/mocks/scenarios/default.ts index 376718f1..c7232c2b 100644 --- a/devex-ui/src/mocks/scenarios/default.ts +++ b/devex-ui/src/mocks/scenarios/default.ts @@ -3,6 +3,7 @@ import { eventStore } from '../stores/event.store'; import { commandStore, recentCommandsStore } from '../stores/command.store'; import { traceStore } from '../stores/trace.store'; import { logStore } from '../stores/log.store'; +import { rolesStore } from '../stores/roles.store'; import { makeEvent } from '../factories/event.factory'; import { makeCommand } from '../factories/command.factory'; import { makeTrace } from '../factories/trace.factory'; @@ -20,6 +21,7 @@ export function loadDefault() { recentCommandsStore.reset(); traceStore.reset(); logStore.reset(); + rolesStore.reset(); // Get seed size from ENV var or use default const SEED_SIZE = Number(import.meta.env.VITE_SEED_SIZE ?? 200); @@ -64,4 +66,42 @@ export function loadDefault() { payload: { systemId: "sys-002" }, response: { success: false, error: "Simulation failed: Network timeout" } } as Command); + + // Seed roles store with domain-specific roles + rolesStore.push({ + id: 'system', + domain: 'system', + roles: ['tester', 'system', 'developer'] + }); + + rolesStore.push({ + id: 'user', + domain: 'user', + roles: ['admin', 'viewer'] + }); + + rolesStore.push({ + id: 'order', + domain: 'order', + roles: ['sales', 'ops'] + }); + + rolesStore.push({ + id: 'payment', + domain: 'payment', + roles: ['billing', 'auditor'] + }); + + rolesStore.push({ + id: 'catalog', + domain: 'catalog', + roles: ['manager', 'editor'] + }); + + rolesStore.push({ + id: 'inventory', + domain: 'inventory', + roles: ['stock', 'restocker'] + }); + } diff --git a/devex-ui/src/mocks/stores/index.ts b/devex-ui/src/mocks/stores/index.ts index ea6d545a..d3391e1e 100644 --- a/devex-ui/src/mocks/stores/index.ts +++ b/devex-ui/src/mocks/stores/index.ts @@ -2,4 +2,5 @@ export * from './command.store' export * from './event.store' export * from './log.store' +export * from './roles.store' export * from './trace.store' diff --git a/devex-ui/src/mocks/stores/roles.store.ts b/devex-ui/src/mocks/stores/roles.store.ts new file mode 100644 index 00000000..20331f06 --- /dev/null +++ b/devex-ui/src/mocks/stores/roles.store.ts @@ -0,0 +1,19 @@ +import { createStore } from './createStore'; + +// Define the role entry type with domain as id to satisfy Identifiable +interface RoleEntry { + id: string; // This will be the domain + domain: string; + roles: string[]; +} + +export const rolesStore = createStore(); + +// Helper function to push a role entry using domain as id +export const addRoleEntry = (domain: string, roles: string[]) => { + rolesStore.push({ + id: domain, + domain, + roles + }); +}; \ No newline at end of file From 223cd695447a12f7fc7f5634bbe19731b68b3f0f Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 02:49:57 -0700 Subject: [PATCH 13/21] feat(ui): improve tenant handling and role selection devex-ui/src/components/CommandIssuer.tsx: Added role validity check with reaction to role changes using `useEffect`. Updated role selection to ensure it's dynamic, clear, and disabled until a command type is selected. --- devex-ui/src/components/CommandIssuer.tsx | 17 ++++++++++++++--- devex-ui/src/components/Header.tsx | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/devex-ui/src/components/CommandIssuer.tsx b/devex-ui/src/components/CommandIssuer.tsx index a8ce494f..76035b29 100644 --- a/devex-ui/src/components/CommandIssuer.tsx +++ b/devex-ui/src/components/CommandIssuer.tsx @@ -280,6 +280,11 @@ export const CommandIssuer = () => { const selectedCommandSchema = commandRegistry.find(cmd => cmd.type === selectedCommand); const domain = selectedCommandSchema?.domain; const { data: roles = [] } = useRoles(domain); + useEffect(() => { + if (role && !roles.includes(role)) { + setRole(null); + } + }, [role, roles, setRole]); const toggleCommandExpansion = (commandId: string) => { setExpandedCommand(expandedCommand === commandId ? null : commandId); @@ -342,9 +347,15 @@ export const CommandIssuer = () => {
- + + {roles.map((r) => ( diff --git a/devex-ui/src/components/Header.tsx b/devex-ui/src/components/Header.tsx index c1b8f756..a1f722c9 100644 --- a/devex-ui/src/components/Header.tsx +++ b/devex-ui/src/components/Header.tsx @@ -13,7 +13,7 @@ import { useAppCtx } from '@/app/AppProvider'; import { isMock, apiMode } from '@/config/apiMode'; import { Logo } from './Logo'; -const tenants = ['tenant-1','0af03580-98d5-4884-96e4-e75168d8b887']; +const tenants = ['0af03580-98d5-4884-96e4-e75168d8b887']; export const Header = () => { const { tenant, setTenant } = useAppCtx(); From eda89c96cb11cbf3887162a23d8ef38ab0149525 Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 03:00:16 -0700 Subject: [PATCH 14/21] docs(ADR): introduce local DevX companion UI ADR src/devex-ui/: Added SPA with tabs for commands, events, projections, traces, and AI assistance. Enabled schema-driven forms, role validation, and simulation/replay functionality through ports-only backend. --- ADRs/022-devx-ui.md | 104 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 ADRs/022-devx-ui.md diff --git a/ADRs/022-devx-ui.md b/ADRs/022-devx-ui.md new file mode 100644 index 00000000..36ab7ba5 --- /dev/null +++ b/ADRs/022-devx-ui.md @@ -0,0 +1,104 @@ +# ADR-022: **Local Side-panel Dev-X Companion** + +## 1 Context + +* The platform uses a ports-first, event-sourced architecture. Commands and events are routed through the same deterministic workflows used in production. +* Developers previously relied on disparate tools (shell, `psql`, Temporal Web UI, Jaeger) to understand or simulate system behavior. +* Postgres `LISTEN` channels (`new_event`, `new_command`) already feed into the local Temporal workers and projections. +* Devs requested a unified, **local-first** DevEx console that enables: + + 1. Inspection, validation, and emission of commands/events + 2. Viewing projection and trace state in real-time + 3. Structured AI-assisted scaffolding (types, sagas, events, projections) + 4. Replay/debug capabilities from the event log +* The UI must reflect actual runtime constraints: multi-tenant, role-restricted command access, and full audit of side effects. + +--- + +## 2-Decision + +### 2.1 High-Level View + +``` +┌─────────────┐ +│ DevEx UI SPA│ ← Tabs: Commands | Events | Traces | Replay | Projections | AI +└─────┬───────┘ + │ REST + SSE/WS +┌─────▼──────────────┐ +│ /api (ports-only) │ ← no direct DB access; calls EventStorePort, SagaRegistry, etc. +│ + /api/ai-proxy │ ← calls LLM with scoped context +└─────┬──────────────┘ + │ +┌─────▼────────────┐ +│ Postgres LISTEN │ ← streams new_command + new_event (if WS) +└─────┬────────────┘ + ▼ + ┌──────────────┐ + │ AI + CLI │ ← Generates `.patch` files for types & handlers (later ADR) + └──────────────┘ +``` + +--- + +### 2.2 Detailed Design Points + +| Aspect | Decision | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **Frontend** | SPA under `src/devex-ui/`; includes tabs for Commands, Events, Projections, Traces, Aggregates, Rewind, AI. | +| **Command issuer** | Fully type-aware with schema-based form generation. Role dropdown adapts dynamically to selected command domain, ensuring only valid roles appear. | +| **Role validation** | If selected role isn’t part of the domain's available roles, it is reset; the role selector UI reflects this with dynamic placeholders (“Select a role” or “Roles populate…”). | +| **Registry source** | `commandRegistry`, `rolesStore`, `eventRegistry`, `projectionRegistry`, `sagaRegistry` are exported from `core/registry.ts`; no hardcoded lists outside source-of-truth. | +| **Backend façade** | `/api/*` routes use ports only (never call DB/Temporal directly). AI and scaffolding features run isolated from runtime mutations. | +| **AI Assist** | `/api/ai-proxy` sends scoped context (selected domain/contract/slice) to LLM. Scaffold output includes `.patch` files only, with no auto-commit. | +| **Replay + simulate** | Event streams are replayed against in-memory aggregates; what-if diffs are computed and shown as JSON state patches. | +| **Security** | No JWT in local mode; RLS still enforced if using a remote DB. | +| **CLI parity** | `devx ui` launches the SPA using same `.env` as CLI + workers. | + +--- + +## 3-Behavior Summary + +### Commands Tab + +* Shows all available commands grouped by domain +* Selecting a command shows schema-driven form and available roles (from `useRoles(domain)`) +* Role selector resets if invalid; UI shows context-aware placeholder +* On submission, command is POSTed to `/api/commands`, processed via normal ports +* After dispatch, the corresponding trace is auto-expanded via correlation ID + +### AI Side-panel + +* **Chat Mode**: query behavior/state over event stream or aggregate +* **Scaffold Mode**: wizard to generate new commands/events/sagas/types +* **Simulator Mode**: run a hypothetical event chain through a rehydrated aggregate and preview resulting state diff + +--- + +## 4-Consequences + +| | Positive | Trade-offs / Mitigations | +| --------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | +| **Unified DevEx dashboard** | Full visibility into all domain slices, including real-time traces, without context switch | WebSocket LISTEN loop may become noisy → supports throttle env var in `.env` | +| **Determinism preserved** | All command submissions flow through existing routing and validation | Incomplete schema = validation failure; schema coverage enforced at registry level | +| **Safe scaffolding** | `.patch` files are generated but not committed | Prevents unsafe auto-writes; CI ensures schema + unit coverage post-patch | +| **Zero infra requirement** | Works with docker-compose stack + local Postgres | If pointing to shared DB, advise dev pods only run LISTEN to avoid shared fan-out noise | +| **Context-aware UI** | Domain → Roles → Payload schema chaining enforced in UI | Role edge cases must be handled when registry mismatches occur (e.g. outdated role list) | + +--- + +## 5-Rejected Alternatives + +| Option | Reason for Rejection | +| ------------------------ | --------------------------------------------------------------------------------------- | +| Full Electron App | Unnecessary packaging overhead; SPA already integrates cleanly with CLI and local stack | +| Retool / External Studio | Breaks invariants, bypasses RLS and command auditing | +| Auto Git Commits | All scaffolds must be reviewed; early .patch outputs are often exploratory | + +--- + +## Future Considerations + +1. Add `useRoles(domain)` + auto-reset logic into CommandIssuer UI +2. Wire WS/SSE `/api/stream/commands` and `/api/stream/events` to feed DevEx console tabs +3. Enable `.patch` output from scaffold mode and render diff preview before saving...AI scaffolding begs for its own ADR +4. Add in-memory replay of event chains into “What-if Simulator” tab...begs for its own ADR From 37e97944d4d9d07a53d47b3291e1eacb1455b1bd Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 03:29:26 -0700 Subject: [PATCH 15/21] feat(ci): add DevX UI build workflow README.md: Added badge for DevX UI build status to enhance visibility. .github/workflows/build.yml: Renamed workflow to clarify its scope as Core and Infra build. .github/workflows/devx-ui-build.yml: Introduced a dedicated CI workflow for DevX UI, including build steps, caching, and dependency installation specific to the devex-ui directory. --- .github/workflows/build.yml | 2 +- .github/workflows/devx-ui-build.yml | 28 ++++++++++++++++++++++++++++ README.md | 1 + 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/devx-ui-build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c194d3c..4d40524c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build +name: Core and Infra Build on: pull_request: diff --git a/.github/workflows/devx-ui-build.yml b/.github/workflows/devx-ui-build.yml new file mode 100644 index 00000000..b64f0019 --- /dev/null +++ b/.github/workflows/devx-ui-build.yml @@ -0,0 +1,28 @@ +name: DevX Build + +on: + pull_request: + branches: + - '**' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + node-version: '22' + cache: 'npm' + + - uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-npm- + + - run: npm ci + working-directory: devex-ui + - run: npm run build + working-directory: devex-ui diff --git a/README.md b/README.md index dc0f3b9e..dfb7044f 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![CI](https://github.com/geeewhy/intent/actions/workflows/unit-tests.yml/badge.svg)](https://github.com/geeewhy/intent/actions/workflows/unit-tests.yml) [![CI](https://github.com/geeewhy/intent/actions/workflows/core-linter.yml/badge.svg)](https://github.com/geeewhy/intent/actions/workflows/core-linter.yml) [![CI](https://github.com/geeewhy/intent/actions/workflows/projection-linter.yml/badge.svg)](https://github.com/geeewhy/intent/actions/workflows/projection-linter.yml) +[![CI](https://github.com/geeewhy/intent/actions/workflows/devx-ui-build.yml/badge.svg)](https://github.com/geeewhy/intent/actions/workflows/devx-ui-build.yml) > **Intent** turns event-sourcing theory into a platform you can demo in five minutes. It’s a pragmatic, ports-first reference for multi-tenant, event-sourced CQRS back-ends powered by TypeScript and uses [Temporal](https://github.com/temporalio/temporal) for durable workflow execution. From 633a0038e5cc3082939330fa96ed145ce4816ef7 Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 04:26:08 -0700 Subject: [PATCH 16/21] feat(core): enhance logging, policy, and linting robustness src/core/policy-registry.ts: Added condition map getter and registered access models for evaluation and debugging purposes, improved policy registration. src/core/logger.ts: Introduced `createLogger` method for scoped logger instances within the core. src/tools/core-lint/index.ts: Expanded linting to validate roles against access models --- src/core/initialize.ts | 2 +- src/core/logger.ts | 10 ++++++++ src/core/policy-registry.ts | 8 ++++++ src/core/system/register.ts | 2 +- src/tools/core-lint/index.ts | 49 +++++++++++++++++++++++++++++++----- 5 files changed, 63 insertions(+), 8 deletions(-) diff --git a/src/core/initialize.ts b/src/core/initialize.ts index 0d97c7d5..8975a93b 100644 --- a/src/core/initialize.ts +++ b/src/core/initialize.ts @@ -11,7 +11,7 @@ import './system/register'; * @returns A promise that resolves when initialization is complete */ export async function initializeCore() { - console.log('Core initialized with:'); + console.log('Core initialized'); console.log(`- ${Object.keys(DomainRegistry.domains()).length} domains`); console.log(`- ${Object.keys(DomainRegistry.aggregates()).length} aggregates`); console.log(`- ${Object.keys(DomainRegistry.commandHandlers()).length} command handlers`); diff --git a/src/core/logger.ts b/src/core/logger.ts index 1de6ac72..47fd0e52 100644 --- a/src/core/logger.ts +++ b/src/core/logger.ts @@ -16,6 +16,16 @@ export function log(): LoggerPort | undefined { } // Helper functions for handlers as examples in core + +export function createLogger(): LoggerPort | undefined { + const logger = log(); + if (!logger) return undefined; + + return logger.child({ + component: 'core' + }); +} + export function createLoggerForCommandHandler(handler: CommandHandler): LoggerPort | undefined { const logger = log(); if (!logger) return undefined; diff --git a/src/core/policy-registry.ts b/src/core/policy-registry.ts index dde6e9b1..fb6d72f3 100644 --- a/src/core/policy-registry.ts +++ b/src/core/policy-registry.ts @@ -29,7 +29,12 @@ export function listRegisteredConditions() { return Object.keys(conditionMap).sort(); } +export function getConditionMap(): Record boolean> { + return { ...conditionMap }; // read-only clone +} + type RoleAccessMap = Record; +export const RegisteredAccessModels: Record = {}; export function registerCommandConditionsFromModel( namespace: string, @@ -47,5 +52,8 @@ export function registerCommandConditionsFromModel( model[role]?.includes(cmd) ); }); + + RegisteredAccessModels[namespace] = model; + return registeredConditions; } diff --git a/src/core/system/register.ts b/src/core/system/register.ts index 52de797d..c66db201 100644 --- a/src/core/system/register.ts +++ b/src/core/system/register.ts @@ -62,7 +62,7 @@ export function registerSystemDomain(): void { }); // Register roles - registerRoles('system', ['tester', 'system', 'developer']); + registerRoles('system', ['tester', 'system', 'developer']); } // Auto-register when imported diff --git a/src/tools/core-lint/index.ts b/src/tools/core-lint/index.ts index 1dbd6bfc..6d576895 100755 --- a/src/tools/core-lint/index.ts +++ b/src/tools/core-lint/index.ts @@ -1,14 +1,16 @@ #!/usr/bin/env node - //src/tools/core-lint/index.ts /** * Lint-core tool * - * Placeholder. - * Currently, checks that all registered commands with a payloadSchema also declare aggregateRouting (if not saga-only) + * 1. Checks that all registered commands with a payloadSchema also declare aggregateRouting (if not saga-only) + * 2. Extracts roles from condition functions and compares them against registered roles */ import { DomainRegistry } from '../../core/registry'; +import {getConditionMap, RegisteredAccessModels} from '../../core/policy-registry'; +import initializeCore from "../../core/initialize"; +void initializeCore(); // Get all registered command types const commandTypes = DomainRegistry.commandTypes(); @@ -27,12 +29,47 @@ Object.entries(commandTypes).forEach(([type, meta]) => { } }); +/** + * OBSOLETE + * Extract roles from a function by analyzing its string representation + * @param fn Function to analyze + * @returns Array of extracted role strings + */ +/*function extractRolesFromFn(fn: Function): string[] { + const src = fn.toString(); + console.log(src); + + const includesMatch = src.match(/\[\s*(['"][^'"]+['"](?:\s*,\s*['"][^'"]+['"])*)\s*\]\.includes\s*\(\s*role\s*\)/); + const equalityMatches = [...src.matchAll(/role\s*[=!]=+\s*['"]([a-zA-Z0-9_-]+)['"]/g)].map(m => m[1]); + + let includesRoles: string[] = []; + if (includesMatch) { + const list = includesMatch[1]; + includesRoles = list.split(',').map(s => s.trim().replace(/['"]/g, '')); + } + + return [...new Set([...includesRoles, ...equalityMatches])]; +}*/ + +// Get registered roles by domain +const rolesByDomain = DomainRegistry.roles(); + +Object.entries(RegisteredAccessModels).forEach(([domain, roleToCmds]) => { + const inferredRoles = Object.keys(roleToCmds); + const declaredRoles = rolesByDomain[domain] ?? []; + + const undeclared = inferredRoles.filter(r => !declaredRoles.includes(r)); + if (undeclared.length > 0) { + issues.push(`Domain '${domain}' uses unregistered roles: ${undeclared.join(', ')}`); + } +}); + // Report results if (issues.length > 0) { - console.error('Payload linting found issues:'); + console.error('❌ Core linting found issues:'); issues.forEach(issue => console.error(`- ${issue}`)); process.exit(1); } else { - console.log('All command payloads have proper routing configuration.'); + console.log('✅ All core linting checks passed.'); process.exit(0); -} \ No newline at end of file +} From 75ad101235ee06823e69614ef328bbcc6e78d0d8 Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 04:27:08 -0700 Subject: [PATCH 17/21] refactor(core-lint): clean up imports --- src/tools/core-lint/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/core-lint/index.ts b/src/tools/core-lint/index.ts index 6d576895..230fafce 100755 --- a/src/tools/core-lint/index.ts +++ b/src/tools/core-lint/index.ts @@ -8,7 +8,7 @@ */ import { DomainRegistry } from '../../core/registry'; -import {getConditionMap, RegisteredAccessModels} from '../../core/policy-registry'; +import {RegisteredAccessModels} from '../../core/policy-registry'; import initializeCore from "../../core/initialize"; void initializeCore(); From 3c685f2b7aa142aab387ad7182cee2ac53019515 Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 04:35:48 -0700 Subject: [PATCH 18/21] docs(tools): add core domain linting tool READMEs --- src/tools/README.md | 1 + src/tools/core-lint/README.md | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/tools/core-lint/README.md diff --git a/src/tools/README.md b/src/tools/README.md index 8275a14c..29136346 100644 --- a/src/tools/README.md +++ b/src/tools/README.md @@ -10,6 +10,7 @@ This directory contains various tools for managing and configuring the platform. | [Projection Drift Check Tool](projections-drift-check/README.md) | Checks for drift between projection definitions in code and actual database tables. | | [Projection Repair Tool](projections-repair/README.md) | Repairs drift between projection definitions in code and actual database tables. | | [Projection RLS Policy Linter](projections-lint/README.md) | Validates the structure, completeness, and correctness of all ReadAccessPolicy definitions. | +| [Core Domain Linter](core-lint/README.md) | Validates that all commands declare routing, and all role-based policies register their required roles. | ## Usage diff --git a/src/tools/core-lint/README.md b/src/tools/core-lint/README.md new file mode 100644 index 00000000..d28d210d --- /dev/null +++ b/src/tools/core-lint/README.md @@ -0,0 +1,37 @@ +# Core Linting Tool + +A utility for validating the consistency of core domain components in the codebase. + +## Usage + +Run the linter with: + +```bash +npm run tool:core-lint +``` + +## What It Checks + +The core linter performs the following validations: + +1. **Command Payload Routing**: Ensures that all registered commands with a payload schema also declare proper aggregate routing (if not saga-only). + +2. **Role Consistency**: Extracts roles from condition functions and compares them against registered roles to identify any unregistered roles being used in access policies. + +## Output + +When all checks pass: +``` +✅ All core linting checks passed. +``` + +When issues are found: +``` +❌ Core linting found issues: +- Command type 'domain.command' has a payloadSchema but no aggregateRouting +- Domain 'domain' uses unregistered roles: role1, role2 +``` + +## Integration + +This tool is integrated into the CI pipeline to ensure code quality and consistency across the codebase. From 54733b22e465e0ba2ccabd1029f3ee194e9cf9d7 Mon Sep 17 00:00:00 2001 From: Gokce Yalcin Date: Tue, 10 Jun 2025 22:09:05 -0700 Subject: [PATCH 19/21] feat(error-handling): enhance error serialization and structure src/infra/logger/stdLogger.ts: Improved errorSerializer with custom fields support and fallback robustness for structured logging. src/core/errors.ts: Refactored BusinessRuleViolation with retryable flag, details, JSON support, and stack trace enhancement. src/api/routes/commands.ts: Added BusinessRuleViolation import for comprehensive error handling. src/infra/contracts.ts, src/infra/temporal/workflows/processCommand.ts: Updated CommandResult error type to Error for consistency. package.json: Added `api:admin:watch` script for improved developer convenience during development. devex-ui/, devex-ui/src/components/CommandIssuer.tsx: Addressed formatting and alignment for consistent code styling. --- devex-ui/src/components/CommandIssuer.tsx | 1097 +++++++++-------- devex-ui/src/data/types.ts | 7 +- package.json | 1 + src/api/routes/commands.ts | 1 + src/core/errors.ts | 47 +- .../system/aggregates/system.aggregate.ts | 15 +- src/infra/contracts.ts | 2 +- src/infra/logger/apiLogger.ts | 3 +- src/infra/logger/stdLogger.ts | 47 +- .../temporal/activities/coreActivities.ts | 9 +- src/infra/temporal/temporal-scheduler.ts | 2 +- src/infra/temporal/workflow-router.ts | 18 +- .../temporal/workflows/processCommand.ts | 2 +- 13 files changed, 676 insertions(+), 575 deletions(-) diff --git a/devex-ui/src/components/CommandIssuer.tsx b/devex-ui/src/components/CommandIssuer.tsx index 76035b29..14fc8894 100644 --- a/devex-ui/src/components/CommandIssuer.tsx +++ b/devex-ui/src/components/CommandIssuer.tsx @@ -1,569 +1,602 @@ //devex-ui/src/components/CommandIssuer.tsx -import { useState, useEffect } from "react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; -import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Badge } from "@/components/ui/badge"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Clock, Send, RotateCcw, Terminal, ChevronDown, ChevronRight, RefreshCw, AlertCircle } from "lucide-react"; -import { useCommands, useSubmitCommand, useRoles } from "@/hooks/api"; -import { validate, registerSchemas } from "@/utils/schemaValidator"; -import { makeExample } from "@/utils/schemaFaker"; -import { toast } from "@/components/ui/sonner"; -import { useAppCtx } from '@/app/AppProvider'; -import { useQuery } from "@tanstack/react-query"; -import { fetchCommandRegistry } from "@/data/apiService"; -import { cn } from "@/lib/utils"; +import {useState, useEffect} from "react"; +import {Button} from "@/components/ui/button"; +import {Input} from "@/components/ui/input"; +import {Label} from "@/components/ui/label"; +import {Textarea} from "@/components/ui/textarea"; +import {Card, CardHeader, CardTitle, CardContent} from "@/components/ui/card"; +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"; +import {Badge} from "@/components/ui/badge"; +import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"; +import {Clock, Send, RotateCcw, Terminal, ChevronDown, ChevronRight, RefreshCw, AlertCircle} from "lucide-react"; +import {useCommands, useSubmitCommand, useRoles} from "@/hooks/api"; +import {validate, registerSchemas} from "@/utils/schemaValidator"; +import {makeExample} from "@/utils/schemaFaker"; +import {toast} from "@/components/ui/sonner"; +import {useAppCtx} from '@/app/AppProvider'; +import {useQuery} from "@tanstack/react-query"; +import {fetchCommandRegistry} from "@/data/apiService"; +import {cn} from "@/lib/utils"; export const CommandIssuer = () => { - const { tenant, role, setRole } = useAppCtx(); - const [selectedCommand, setSelectedCommand] = useState(""); - const [payload, setPayload] = useState(""); - const [formData, setFormData] = useState>({}); - const [payloadView, setPayloadView] = useState<"form" | "json">("form"); - const [expandedCommand, setExpandedCommand] = useState(null); - const [validationErrors, setValidationErrors] = useState([]); - const [invalidFields, setInvalidFields] = useState>(new Set()); - const [lastSubmittedId, setLastSubmittedId] = useState(null); - const [highlightedCommandId, setHighlightedCommandId] = useState(null); - - // Use React Query hooks - const { data: recentCommands = [] } = useCommands(tenant, 10); - const { mutate: submitCommandMutation, isPending: isSubmitting } = useSubmitCommand(); - const { data: commandRegistry = [], isLoading: isLoadingRegistry } = useQuery({ - queryKey: ['commandRegistry'], - queryFn: fetchCommandRegistry, - staleTime: Infinity - }); - - // Register schemas when registry is loaded - useEffect(() => { - if (commandRegistry.length > 0) { - registerSchemas(commandRegistry); - } - }, [commandRegistry]); - - // Extract field names from validation error messages - const extractFieldNames = (errors: string[]): Set => { - const fieldNames = new Set(); - errors.forEach(error => { - // Common AJV error patterns like "must have required property 'fieldName'" or "should be string (fieldName)" - const requiredMatch = error.match(/must have required property '(\w+)'/); - const typeMatch = error.match(/should be \w+ \((\w+)\)/); - - if (requiredMatch && requiredMatch[1]) { - fieldNames.add(requiredMatch[1]); - } else if (typeMatch && typeMatch[1]) { - fieldNames.add(typeMatch[1]); - } + const {tenant, role, setRole} = useAppCtx(); + const [selectedCommand, setSelectedCommand] = useState(""); + const [payload, setPayload] = useState(""); + const [formData, setFormData] = useState>({}); + const [payloadView, setPayloadView] = useState<"form" | "json">("form"); + const [expandedCommand, setExpandedCommand] = useState(null); + const [validationErrors, setValidationErrors] = useState([]); + const [invalidFields, setInvalidFields] = useState>(new Set()); + const [lastSubmittedId, setLastSubmittedId] = useState(null); + const [highlightedCommandId, setHighlightedCommandId] = useState(null); + + // Use React Query hooks + const {data: recentCommands = []} = useCommands(tenant, 10); + const {mutate: submitCommandMutation, isPending: isSubmitting} = useSubmitCommand(); + const {data: commandRegistry = [], isLoading: isLoadingRegistry} = useQuery({ + queryKey: ['commandRegistry'], + queryFn: fetchCommandRegistry, + staleTime: Infinity }); - return fieldNames; - }; - - const handleSubmit = () => { - // Clear previous validation errors - setValidationErrors([]); - setInvalidFields(new Set()); - - // Get the payload based on the current view - const payloadData = payloadView === "form" ? formData : (() => { - try { - return JSON.parse(payload || '{}'); - } catch (e) { - setValidationErrors(['Invalid JSON format']); - toast.error('Invalid JSON format', { - description: 'Please check your JSON syntax and try again.' + + // Register schemas when registry is loaded + useEffect(() => { + if (commandRegistry.length > 0) { + registerSchemas(commandRegistry); + } + }, [commandRegistry]); + + // Extract field names from validation error messages + const extractFieldNames = (errors: string[]): Set => { + const fieldNames = new Set(); + errors.forEach(error => { + // Common AJV error patterns like "must have required property 'fieldName'" or "should be string (fieldName)" + const requiredMatch = error.match(/must have required property '(\w+)'/); + const typeMatch = error.match(/should be \w+ \((\w+)\)/); + + if (requiredMatch && requiredMatch[1]) { + fieldNames.add(requiredMatch[1]); + } else if (typeMatch && typeMatch[1]) { + fieldNames.add(typeMatch[1]); + } + }); + return fieldNames; + }; + + const handleSubmit = () => { + // Clear previous validation errors + setValidationErrors([]); + setInvalidFields(new Set()); + + // Get the payload based on the current view + const payloadData = payloadView === "form" ? formData : (() => { + try { + return JSON.parse(payload || '{}'); + } catch (e) { + setValidationErrors(['Invalid JSON format']); + toast.error('Invalid JSON format', { + description: 'Please check your JSON syntax and try again.' + }); + return null; + } + })(); + + // If JSON parsing failed, don't proceed + if (payloadData === null) return; + + // Validate the payload against the command schema + const {ok, errors} = validate(selectedCommand, payloadData); + + if (!ok) { + setValidationErrors(errors); + + // Extract field names from errors and set invalid fields + const fieldNames = extractFieldNames(errors); + setInvalidFields(fieldNames); + + // Show toast with validation errors + toast.error('Command validation failed', { + description: ( +
    + {errors.map((error, index) => ( +
  • {error}
  • + ))} +
+ ) + }); + + return; + } + + const commandPayload = { + id: crypto.randomUUID(), + tenant_id: tenant, + type: selectedCommand, + payload: payloadData, + metadata: { + timestamp: new Date().toISOString(), + userId: crypto.randomUUID(), + role, + source: 'devx/command-issuer' + } + }; + + console.log('Submitting command:', commandPayload); + + submitCommandMutation(commandPayload, { + onSuccess: (result) => { + console.log('Command submission result:', result); + + if (result.status === 'success') { + toast.success('Command executed', { + description: `${result.events?.length > 0 ? result.events?.length + 'event(s) produced' : ''}` + }); + + // Set the last submitted ID + setLastSubmittedId(commandPayload.id); + setHighlightedCommandId(commandPayload.id); + + setTimeout(() => { + setHighlightedCommandId(null); + }, 500); + + // Reset form + setPayload(""); + setFormData({}); + setValidationErrors([]); + setInvalidFields(new Set()); + } else { + const msg = + result.error?.name + ? `${result.error.name || 'Error'}: ${result.error.message}` + : result.error?.message ?? 'Unknown error'; + + const details = result.error?.details?.message; + + toast.error('Command failed', { + description: ( + <> + {msg} + {details ? ( + <> +
+ {details} + + ) : null} + + ), + }); + } + }, + onError: (error) => { + console.error('Command submission failed:', error); + + // Show error toast + toast.error('Command submission failed', { + description: error instanceof Error ? error.message : 'An unknown error occurred' + }); + } }); - return null; - } - })(); - - // If JSON parsing failed, don't proceed - if (payloadData === null) return; - - // Validate the payload against the command schema - const { ok, errors } = validate(selectedCommand, payloadData); - - if (!ok) { - setValidationErrors(errors); - - // Extract field names from errors and set invalid fields - const fieldNames = extractFieldNames(errors); - setInvalidFields(fieldNames); - - // Show toast with validation errors - toast.error('Command validation failed', { - description: ( -
    - {errors.map((error, index) => ( -
  • {error}
  • - ))} -
- ) - }); - - return; - } - - const commandPayload = { - id: crypto.randomUUID(), - tenant_id: tenant, - type: selectedCommand, - payload: payloadData, - metadata: { - timestamp: new Date().toISOString(), - userId: crypto.randomUUID(), - role, - source: 'devx/command-issuer' - } }; - console.log('Submitting command:', commandPayload); - submitCommandMutation(commandPayload, { - onSuccess: (result) => { - console.log('Command submission result:', result); + const handleFormDataChange = (key: string, value: unknown) => { + const newFormData = {...formData, [key]: value}; + setFormData(newFormData); + setPayload(JSON.stringify(newFormData, null, 2)); + }; - if (result.status === 'success') { - toast.success('Command executed', { - description: `${result.events?.length > 0 ? result.events?.length + 'event(s) produced' : ''}` - }); + const handlePayloadChange = (value: string) => { + setPayload(value); + try { + const parsed = JSON.parse(value); + setFormData(parsed); + } catch (e) { + // Invalid JSON, keep form data as is + } + }; - // Set the last submitted ID - setLastSubmittedId(commandPayload.id); - setHighlightedCommandId(commandPayload.id); - setTimeout(() => { - setHighlightedCommandId(null); - }, 500); + const renderFormField = (key: string, prop: Record, required: boolean) => { + const value = formData[key] || ''; + const isIdField = key.includes('Id'); + const isInvalid = invalidFields.has(key); - // Reset form - setPayload(""); - setFormData({}); - setValidationErrors([]); - setInvalidFields(new Set()); - } else { - toast.error('Command failed', { - description: 'Error:' + result.error || 'Unknown failure' - }); + switch (prop.type) { + case 'string': + return ( +
+ +
+ handleFormDataChange(key, e.target.value)} + placeholder={`Enter ${key}`} + className={`bg-slate-800 ${isInvalid ? 'border-red-500' : 'border-slate-700'} text-slate-100 h-8 ${isInvalid ? 'focus-visible:ring-red-500' : ''}`} + /> + {isIdField && ( + + )} +
+
+ ); + case 'number': + return ( +
+ + handleFormDataChange(key, Number(e.target.value))} + placeholder={`Enter ${key}`} + className={`bg-slate-800 ${isInvalid ? 'border-red-500' : 'border-slate-700'} text-slate-100 h-8 ${isInvalid ? 'focus-visible:ring-red-500' : ''}`} + /> +
+ ); + case 'object': + return ( +
+ +