diff --git a/bun.lock b/bun.lock index 866807792b..57e3823b57 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 1, "workspaces": { "": { "name": "@metorial/integrations-root", @@ -2723,7 +2722,7 @@ }, "integrations/confluence": { "name": "@slates-integrations/confluence", - "version": "0.2.0-rc.7", + "version": "0.2.0-rc.8", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -4556,7 +4555,7 @@ }, "integrations/firebase": { "name": "@slates-integrations/firebase", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -4967,7 +4966,7 @@ }, "integrations/gemini": { "name": "@slates-integrations/gemini", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -5941,7 +5940,7 @@ }, "integrations/heroku": { "name": "@slates-integrations/heroku", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -6414,7 +6413,7 @@ }, "integrations/instagram": { "name": "@slates-integrations/instagram", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -6451,7 +6450,7 @@ }, "integrations/intercom": { "name": "@slates-integrations/intercom", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -7439,7 +7438,7 @@ }, "integrations/mailchimp": { "name": "@slates-integrations/mailchimp", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -7692,7 +7691,7 @@ }, "integrations/messenger": { "name": "@slates-integrations/facebook-messenger", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -8175,7 +8174,7 @@ }, "integrations/netlify": { "name": "@slates-integrations/netlify", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.7", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -9206,7 +9205,7 @@ }, "integrations/pipedrive": { "name": "@slates-integrations/pipedrive", - "version": "0.2.0-rc.6", + "version": "0.2.0-rc.7", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -9423,7 +9422,7 @@ }, "integrations/postgresql": { "name": "@slates-integrations/postgresql", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -9739,7 +9738,7 @@ }, "integrations/quickbooks": { "name": "@slates-integrations/quickbooks", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -9896,7 +9895,7 @@ }, "integrations/reddit": { "name": "@slates-integrations/reddit", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -11440,19 +11439,6 @@ "typescript": "^5", }, }, - "integrations/statbank-norway-ssb": { - "name": "@slates-integrations/statbank-norway-ssb", - "version": "0.1.0-rc.3", - "dependencies": { - "@lowerdeck/error": "^1.1.0", - "@types/node": "^20", - "slates": "1.0.0-rc.10", - "zod": "^4.2", - }, - "devDependencies": { - "typescript": "^5", - }, - }, "integrations/sslmate-cert-spotter-api": { "name": "@slates-integrations/sslmate-cert-spotter-api", "version": "0.2.0-rc.2", @@ -11525,6 +11511,19 @@ "typescript": "^5", }, }, + "integrations/statbank-norway-ssb": { + "name": "@slates-integrations/statbank-norway-ssb", + "version": "0.1.0-rc.3", + "dependencies": { + "@lowerdeck/error": "^1.1.0", + "@types/node": "^20", + "slates": "1.0.0-rc.10", + "zod": "^4.2", + }, + "devDependencies": { + "typescript": "^5", + }, + }, "integrations/statuscake": { "name": "@slates-integrations/statuscake", "version": "0.2.0-rc.2", @@ -11549,6 +11548,23 @@ "typescript": "^5", }, }, + "integrations/stern-financial-data": { + "name": "@slates-integrations/stern-financial-data", + "version": "0.1.0-rc.1", + "dependencies": { + "@lowerdeck/error": "^1.1.0", + "@mixmark-io/domino": "^2.2.0", + "@types/node": "^20", + "slates": "1.0.0-rc.10", + "xlsx": "^0.18.5", + "zod": "^4.2", + }, + "devDependencies": { + "@slates/test": "1.0.0-rc.4", + "typescript": "^5", + "vitest": "^3.1.2", + }, + }, "integrations/stitch": { "name": "@slates-integrations/stitch", "version": "0.2.0-rc.2", @@ -12531,7 +12547,7 @@ }, "integrations/typeform": { "name": "@slates-integrations/typeform", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.7", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -13267,7 +13283,7 @@ }, "integrations/xero": { "name": "@slates-integrations/xero", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.8", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -13345,7 +13361,7 @@ }, "integrations/zapier": { "name": "@slates-integrations/zapier", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.6", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -13540,7 +13556,7 @@ }, "integrations/zoom": { "name": "@slates-integrations/zoom", - "version": "0.2.0-rc.5", + "version": "0.2.0-rc.7", "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/provider": "1.0.0-rc.11", @@ -14305,6 +14321,8 @@ "@lowerdeck/validation": ["@lowerdeck/validation@1.0.10", "", {}, "sha512-M6F1+SRuxUKkgR9qPi3EhO5+WUXckAxhRnNYCtp1CbFl0HZSV+IIOdvfVq29RRn7TIObB2/rYvqJQ38tSHq6Pw=="], + "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], + "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], "@rollup/plugin-alias": ["@rollup/plugin-alias@3.1.9", "", { "dependencies": { "slash": "^3.0.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0" } }, "sha512-QI5fsEvm9bDzt32k39wpOwZhVzRcL5ydcffUHMyLVaVaLeC70I8TJZ17F1z1eMoLu4E/UOcH9BWVkKpIKdrfiw=="], @@ -16255,8 +16273,6 @@ "@slates-integrations/squarespace": ["@slates-integrations/squarespace@workspace:integrations/squarespace"], - "@slates-integrations/statbank-norway-ssb": ["@slates-integrations/statbank-norway-ssb@workspace:integrations/statbank-norway-ssb"], - "@slates-integrations/sslmate-cert-spotter-api": ["@slates-integrations/sslmate-cert-spotter-api@workspace:integrations/sslmate-cert-spotter-api"], "@slates-integrations/stability-ai": ["@slates-integrations/stability-ai@workspace:integrations/stability-ai"], @@ -16269,10 +16285,14 @@ "@slates-integrations/starton": ["@slates-integrations/starton@workspace:integrations/starton"], + "@slates-integrations/statbank-norway-ssb": ["@slates-integrations/statbank-norway-ssb@workspace:integrations/statbank-norway-ssb"], + "@slates-integrations/statuscake": ["@slates-integrations/statuscake@workspace:integrations/statuscake"], "@slates-integrations/statuspage": ["@slates-integrations/statuspage@workspace:integrations/statuspage"], + "@slates-integrations/stern-financial-data": ["@slates-integrations/stern-financial-data@workspace:integrations/stern-financial-data"], + "@slates-integrations/stitch": ["@slates-integrations/stitch@workspace:integrations/stitch"], "@slates-integrations/stoat": ["@slates-integrations/stoat@workspace:integrations/stoat"], @@ -16777,6 +16797,8 @@ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + "adler-32": ["adler-32@1.3.1", "", {}, "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A=="], + "ansi-regex": ["ansi-regex@2.1.1", "", {}, "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA=="], "ansi-styles": ["ansi-styles@2.2.1", "", {}, "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA=="], @@ -16853,6 +16875,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001792", "", {}, "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw=="], + "cfb": ["cfb@1.2.2", "", { "dependencies": { "adler-32": "~1.3.0", "crc-32": "~1.2.0" } }, "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA=="], + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="], @@ -16867,6 +16891,8 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "codepage": ["codepage@1.15.0", "", {}, "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], @@ -16893,6 +16919,8 @@ "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], + "css-declaration-sorter": ["css-declaration-sorter@6.4.1", "", { "peerDependencies": { "postcss": "^8.0.9" } }, "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g=="], "css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], @@ -17007,6 +17035,8 @@ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "frac": ["frac@1.1.2", "", {}, "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="], + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], @@ -17453,6 +17483,8 @@ "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], + "ssf": ["ssf@0.11.2", "", { "dependencies": { "frac": "~1.1.2" } }, "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g=="], + "stable": ["stable@0.1.8", "", {}, "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -17567,10 +17599,16 @@ "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "wmf": ["wmf@1.0.2", "", {}, "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="], + + "word": ["word@0.3.0", "", {}, "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "xlsx": ["xlsx@0.18.5", "", { "dependencies": { "adler-32": "~1.3.0", "cfb": "~1.2.1", "codepage": "~1.15.0", "crc-32": "~1.2.1", "ssf": "~0.11.2", "wmf": "~1.0.1", "word": "~0.3.0" }, "bin": { "xlsx": "bin/xlsx.njs" } }, "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ=="], + "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -19521,6 +19559,8 @@ "@slates-integrations/statuspage/slates": ["slates@1.0.0-rc.9", "", { "dependencies": { "@slates/provider": "1.0.0-rc.10" } }, "sha512-fwuT9dcSmW6J/O84niSrj3X992XLtniaZR7XapimUKwmH/nZhHaiuglQoJ9WUi+P3cLv9R3SFqAMkjODTp5wpQ=="], + "@slates-integrations/stern-financial-data/@slates/test": ["@slates/test@1.0.0-rc.4", "", { "dependencies": { "@lowerdeck/testing-tools": "latest", "@slates/client": "1.0.0-rc.6", "@slates/profiles": "1.0.0-rc.4" } }, "sha512-gCh8IL15DvKiNiTjejm1KfifwtrPmMQ0xgQ4Hzk2/6PoaUBL7fzbKi1vLRiNd+7F7wvknnz04Y22HqpA++llgw=="], + "@slates-integrations/stitch/slates": ["slates@1.0.0-rc.9", "", { "dependencies": { "@slates/provider": "1.0.0-rc.10" } }, "sha512-fwuT9dcSmW6J/O84niSrj3X992XLtniaZR7XapimUKwmH/nZhHaiuglQoJ9WUi+P3cLv9R3SFqAMkjODTp5wpQ=="], "@slates-integrations/stoat/slates": ["slates@1.0.0-rc.9", "", { "dependencies": { "@slates/provider": "1.0.0-rc.10" } }, "sha512-fwuT9dcSmW6J/O84niSrj3X992XLtniaZR7XapimUKwmH/nZhHaiuglQoJ9WUi+P3cLv9R3SFqAMkjODTp5wpQ=="], @@ -21843,6 +21883,10 @@ "@slates-integrations/statuspage/slates/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="], + "@slates-integrations/stern-financial-data/@slates/test/@slates/client": ["@slates/client@1.0.0-rc.6", "", { "dependencies": { "@slates/proto": "1.0.0-rc.8", "@slates/provider": "1.0.0-rc.10", "@slates/provider-handler": "1.0.0-rc.9" } }, "sha512-y9DONyNvs5sxjebi0QkejwCmVV8msXdMidu+2M6rxCHKdq05L8WnxazpsDN+OxWSmrs9+fgISfZVlPKYl51LWw=="], + + "@slates-integrations/stern-financial-data/@slates/test/@slates/profiles": ["@slates/profiles@1.0.0-rc.4", "", { "dependencies": { "@slates/client": "1.0.0-rc.6", "@slates/proto": "1.0.0-rc.8" } }, "sha512-VrBG1Fo1XmqMwPfaG6ZEikYp5nSDz4RJ7puRAAfhQLLM2Pf3kHtpb/NAiBmdJe9dz2UNLr0+wGzq/fyp61pURA=="], + "@slates-integrations/stitch/slates/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="], "@slates-integrations/stoat/slates/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="], @@ -22429,6 +22473,14 @@ "@slates-integrations/slack/@slates/test/@slates/profiles/@slates/proto": ["@slates/proto@1.0.0-rc.8", "", { "dependencies": { "@lowerdeck/emitter": "^1.0.4", "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-3PB2SCFi13mYNkkptX9dbz8xOFgEdtXd6atDtaaIDs8vSxIMWy8C9JoSqtqXDmJOfM8Jk/dCSWxjoTPEYnMfgQ=="], + "@slates-integrations/stern-financial-data/@slates/test/@slates/client/@slates/proto": ["@slates/proto@1.0.0-rc.8", "", { "dependencies": { "@lowerdeck/emitter": "^1.0.4", "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-3PB2SCFi13mYNkkptX9dbz8xOFgEdtXd6atDtaaIDs8vSxIMWy8C9JoSqtqXDmJOfM8Jk/dCSWxjoTPEYnMfgQ=="], + + "@slates-integrations/stern-financial-data/@slates/test/@slates/client/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="], + + "@slates-integrations/stern-financial-data/@slates/test/@slates/client/@slates/provider-handler": ["@slates/provider-handler@1.0.0-rc.9", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@slates/proto": "1.0.0-rc.8", "@slates/provider": "1.0.0-rc.10", "zod": "^4.2.1" } }, "sha512-F63WHfzS050WLpZbQ/mRq4dRA7aHhcrahjvCluuH6rqwOASIo5aPf1QCPvktvTA7ufxfnscZe0uKiW3IFtsuzA=="], + + "@slates-integrations/stern-financial-data/@slates/test/@slates/profiles/@slates/proto": ["@slates/proto@1.0.0-rc.8", "", { "dependencies": { "@lowerdeck/emitter": "^1.0.4", "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-3PB2SCFi13mYNkkptX9dbz8xOFgEdtXd6atDtaaIDs8vSxIMWy8C9JoSqtqXDmJOfM8Jk/dCSWxjoTPEYnMfgQ=="], + "@slates-integrations/youtube-analytics/@slates/test/@slates/client/@slates/proto": ["@slates/proto@1.0.0-rc.8", "", { "dependencies": { "@lowerdeck/emitter": "^1.0.4", "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-3PB2SCFi13mYNkkptX9dbz8xOFgEdtXd6atDtaaIDs8vSxIMWy8C9JoSqtqXDmJOfM8Jk/dCSWxjoTPEYnMfgQ=="], "@slates-integrations/youtube-analytics/@slates/test/@slates/client/@slates/provider": ["@slates/provider@1.0.0-rc.10", "", { "dependencies": { "@lowerdeck/error": "^1.1.0", "@toon-format/toon": "^2.1.0", "axios": "^1.13.2", "zod": "^4.2.1" } }, "sha512-6Kvzj6tsCAlrUEFIUepIM68gGixLXufgYZeMr/IpU0Pw5xcjpIsFAV0Er5bAuXd4G17RrQ2oxO9UyetU7GNmCw=="], diff --git a/integrations/confluence/package.json b/integrations/confluence/package.json index d013b5d34d..4b574c2142 100644 --- a/integrations/confluence/package.json +++ b/integrations/confluence/package.json @@ -15,5 +15,5 @@ "devDependencies": { "typescript": "^5" }, - "version": "0.2.0-rc.7" + "version": "0.2.0-rc.8" } diff --git a/integrations/confluence/src/lib/client.ts b/integrations/confluence/src/lib/client.ts index 70a1e5de9e..d9e0054790 100644 --- a/integrations/confluence/src/lib/client.ts +++ b/integrations/confluence/src/lib/client.ts @@ -28,7 +28,7 @@ export interface ConfluencePage { status: string; title: string; spaceId?: string; - parentId?: string; + parentId?: string | null; authorId?: string; createdAt?: string; version?: { number: number; message?: string; createdAt?: string }; diff --git a/integrations/confluence/src/tools/get-page.ts b/integrations/confluence/src/tools/get-page.ts index 2a188c9f27..3aa0f889cd 100644 --- a/integrations/confluence/src/tools/get-page.ts +++ b/integrations/confluence/src/tools/get-page.ts @@ -47,7 +47,7 @@ export let getPage = SlateTool.create(spec, { title: page.title, status: page.status, spaceId: page.spaceId, - parentId: page.parentId, + parentId: page.parentId ?? undefined, authorId: page.authorId, createdAt: page.createdAt, versionNumber: page.version?.number, diff --git a/integrations/confluence/src/tools/list-pages.ts b/integrations/confluence/src/tools/list-pages.ts index be75e6d850..7e108058a2 100644 --- a/integrations/confluence/src/tools/list-pages.ts +++ b/integrations/confluence/src/tools/list-pages.ts @@ -67,7 +67,7 @@ export let listPages = SlateTool.create(spec, { title: p.title, status: p.status, spaceId: p.spaceId, - parentId: p.parentId, + parentId: p.parentId ?? undefined, versionNumber: p.version?.number, createdAt: p.createdAt })); diff --git a/integrations/stern-financial-data/README.md b/integrations/stern-financial-data/README.md new file mode 100644 index 0000000000..deb2a4ca16 --- /dev/null +++ b/integrations/stern-financial-data/README.md @@ -0,0 +1,31 @@ +# Stern Financial Data + +Query public NYU Stern financial datasets from Aswath Damodaran's data pages. + +This integration reads public workbook datasets and falls back to the matching +HTML table when workbook extraction fails. No authentication is required. + +## Tools + +- `list_sources`: list supported Stern data sources, fields, source URLs, and filter hints. +- `get_source`: retrieve one source and return filtered rows. Full output is available + with `returnAll`, but filters and limits are recommended because rows are wide. + +## Sources + +- `erp`: country equity risk premiums, default spreads, corporate tax rates, and CDS data. +- `us_industry_betas`: US industry beta, leverage, tax, cash, risk, and volatility data. +- `global_industry_betas`: global industry beta, leverage, tax, cash, risk, and volatility data. + +## Authentication + +No authentication is required. The package uses `addNone()` and calls the public +Stern pages and workbooks directly. + +## License + +This integration is licensed under the [FSL-1.1](https://github.com/metorial/metorial-platform/blob/dev/LICENSE). + +
+ Built with ❤️ by Metorial +
diff --git a/integrations/stern-financial-data/docs/SPEC.md b/integrations/stern-financial-data/docs/SPEC.md new file mode 100644 index 0000000000..568ca16159 --- /dev/null +++ b/integrations/stern-financial-data/docs/SPEC.md @@ -0,0 +1,67 @@ +# Slates Specification for Stern Financial Data + +## Overview + +Stern Financial Data reads public datasets from NYU Stern's Aswath Damodaran +data pages. The integration focuses on compact discovery and filtered reads for +large financial tables. + +Supported sources: + +- `erp`: country equity risk premium data from `ctryprem`. +- `us_industry_betas`: US industry beta data from `betas`. +- `global_industry_betas`: global industry beta data from `betaGlobal`. + +## Authentication + +These Stern datasets are public. No API key, OAuth flow, token, or account +configuration is needed. The integration uses the no-auth Slate authentication +method. + +## Extraction + +Each source has a public HTML page and workbook URL. `get_source` retrieves the +HTML page, then tries the workbook first because workbook cells preserve numeric +values and formatting. If workbook fetch or parsing fails, the tool falls back +to parsing the HTML table with the same header aliases and type coercion. + +Percent values are returned as decimal numbers, for example `4.23%` becomes +`0.0423`. Original cell text can be included with `includeRaw`. + +## Tools + +### list_sources + +Returns the supported source ids, source titles, page/workbook URLs, row fields, +and supported filters. + +### get_source + +Retrieves one source and applies source-specific filters. + +ERP filters: + +- exact country list or country text search +- Moody's rating list +- equity risk premium range +- country risk premium range +- corporate tax rate range +- sovereign CDS presence + +Industry beta filters: + +- exact industry list or industry text search +- row type: `industry` or `aggregate` +- beta range +- unlevered beta range +- minimum number of firms +- maximum debt-to-equity ratio + +By default the tool returns up to 25 filtered rows. Use `returnAll: true` only +when full output is needed; filtered reads are preferred because rows are wide +and include many financial metrics. + +## Events + +The provider does not support webhooks or event subscriptions for these public +datasets. This integration is read-only and tool-only. diff --git a/integrations/stern-financial-data/logo.svg b/integrations/stern-financial-data/logo.svg new file mode 100644 index 0000000000..abdc0a9854 --- /dev/null +++ b/integrations/stern-financial-data/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/integrations/stern-financial-data/package.json b/integrations/stern-financial-data/package.json new file mode 100644 index 0000000000..b32ada5881 --- /dev/null +++ b/integrations/stern-financial-data/package.json @@ -0,0 +1,24 @@ +{ + "name": "@slates-integrations/stern-financial-data", + "main": "src/index.ts", + "type": "module", + "scripts": { + "build": "bunx @vercel/ncc build src/index.ts -o dist -m -s", + "test": "vitest run --passWithNoTests", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@lowerdeck/error": "^1.1.0", + "@mixmark-io/domino": "^2.2.0", + "@types/node": "^20", + "slates": "1.0.0-rc.10", + "xlsx": "^0.18.5", + "zod": "^4.2" + }, + "devDependencies": { + "@slates/test": "1.0.0-rc.4", + "typescript": "^5", + "vitest": "^3.1.2" + }, + "version": "0.1.0-rc.1" +} diff --git a/integrations/stern-financial-data/slate.json b/integrations/stern-financial-data/slate.json new file mode 100644 index 0000000000..1dec9090bb --- /dev/null +++ b/integrations/stern-financial-data/slate.json @@ -0,0 +1,13 @@ +{ + "name": "@metorial/stern-financial-data", + "description": "Query public NYU Stern financial datasets including country equity risk premiums and industry betas.", + "categories": ["finance-and-accounting", "data-and-analytics"], + "skills": [ + "list NYU Stern financial datasets", + "query country equity risk premiums", + "filter country risk premium data", + "query US industry betas", + "query global industry betas", + "filter industry beta datasets" + ] +} diff --git a/integrations/stern-financial-data/src/auth.ts b/integrations/stern-financial-data/src/auth.ts new file mode 100644 index 0000000000..0368388dae --- /dev/null +++ b/integrations/stern-financial-data/src/auth.ts @@ -0,0 +1,4 @@ +import { SlateAuth } from 'slates'; +import { z } from 'zod'; + +export let auth = SlateAuth.create().output(z.object({})).addNone(); diff --git a/integrations/stern-financial-data/src/config.ts b/integrations/stern-financial-data/src/config.ts new file mode 100644 index 0000000000..f32a0aed8f --- /dev/null +++ b/integrations/stern-financial-data/src/config.ts @@ -0,0 +1,4 @@ +import { SlateConfig } from 'slates'; +import { z } from 'zod'; + +export let config = SlateConfig.create(z.object({})); diff --git a/integrations/stern-financial-data/src/index.ts b/integrations/stern-financial-data/src/index.ts new file mode 100644 index 0000000000..049865f875 --- /dev/null +++ b/integrations/stern-financial-data/src/index.ts @@ -0,0 +1,9 @@ +import { Slate } from 'slates'; +import { spec } from './spec'; +import { getSource, listSources } from './tools'; + +export let provider = Slate.create({ + spec, + tools: [listSources, getSource], + triggers: [] +}); diff --git a/integrations/stern-financial-data/src/lib/client.ts b/integrations/stern-financial-data/src/lib/client.ts new file mode 100644 index 0000000000..db29b8ff77 --- /dev/null +++ b/integrations/stern-financial-data/src/lib/client.ts @@ -0,0 +1,111 @@ +import { sternFinancialDataApiError, sternFinancialDataServiceError } from './errors'; +import { extractRowsForSource, parseWorkbook } from './extractor'; +import { SOURCES, SourceId, SourceType, SternRow } from './sources'; + +export type SourceResult = { + metadata: { + source: SourceId; + title: string; + pageUrl: string; + workbookUrl: string; + retrievedAt: string; + sourceType: SourceType; + workbookFallbackReason?: string; + }; + rows: SternRow[]; +}; + +let FETCH_TIMEOUT_MS = 20_000; + +let fetchWithTimeout = async (url: string, operation: string) => { + let controller = new AbortController(); + let timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); + + try { + return await fetch(url, { signal: controller.signal }); + } catch (error) { + if (controller.signal.aborted) { + throw sternFinancialDataServiceError( + `Stern Financial Data ${operation} timed out after ${FETCH_TIMEOUT_MS}ms.` + ); + } + + throw error; + } finally { + clearTimeout(timeout); + } +}; + +let fetchText = async (url: string) => { + try { + let response = await fetchWithTimeout(url, 'HTML page fetch'); + + if (!response.ok) { + throw Object.assign(new Error(`${response.status} ${response.statusText}`), { + status: response.status, + statusText: response.statusText + }); + } + + return await response.text(); + } catch (error) { + throw sternFinancialDataApiError(error, 'HTML page fetch'); + } +}; + +let fetchBuffer = async (url: string) => { + try { + let response = await fetchWithTimeout(url, 'workbook fetch'); + + if (!response.ok) { + throw Object.assign(new Error(`${response.status} ${response.statusText}`), { + status: response.status, + statusText: response.statusText + }); + } + + return Buffer.from(await response.arrayBuffer()); + } catch (error) { + throw sternFinancialDataApiError(error, 'workbook fetch'); + } +}; + +let errorMessage = (error: unknown) => + error instanceof Error ? error.message : String(error); + +export class SternFinancialDataClient { + async getSource(sourceId: SourceId): Promise { + let source = SOURCES[sourceId]; + let retrievedAt = new Date().toISOString(); + let pageHtml = await fetchText(source.pageUrl); + let rows: SternRow[] = []; + let sourceType: SourceType = 'workbook'; + let workbookFallbackReason: string | undefined; + + try { + let workbook = parseWorkbook(await fetchBuffer(source.workbookUrl)); + rows = extractRowsForSource(sourceId, workbook, pageHtml); + } catch (error) { + workbookFallbackReason = errorMessage(error); + sourceType = 'html'; + rows = extractRowsForSource(sourceId, null, pageHtml); + } + + if (rows.length === 0) { + throw sternFinancialDataServiceError(`No rows extracted for Stern source ${sourceId}.`); + } + + return { + metadata: { + source: sourceId, + title: source.title, + pageUrl: source.pageUrl, + workbookUrl: source.workbookUrl, + retrievedAt, + sourceType, + workbookFallbackReason + }, + rows + }; + } +} diff --git a/integrations/stern-financial-data/src/lib/errors.ts b/integrations/stern-financial-data/src/lib/errors.ts new file mode 100644 index 0000000000..62278e5c45 --- /dev/null +++ b/integrations/stern-financial-data/src/lib/errors.ts @@ -0,0 +1,93 @@ +import { ServiceError, badRequestError } from '@lowerdeck/error'; + +type ErrorResponse = { + status?: number; + statusText?: string; + data?: unknown; +}; + +let isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null; + +let addMessage = (messages: string[], value: unknown) => { + if (typeof value !== 'string') return; + + let trimmed = value.trim(); + if (trimmed && !messages.includes(trimmed)) { + messages.push(trimmed); + } +}; + +let collectMessages = (value: unknown, messages: string[]) => { + if (!isRecord(value)) { + addMessage(messages, value); + return; + } + + for (let key of ['message', 'error', 'detail', 'description', 'title']) { + addMessage(messages, value[key]); + } + + if (Array.isArray(value.errors)) { + for (let item of value.errors) { + collectMessages(item, messages); + } + } +}; + +let extractMessage = (error: unknown) => { + let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined; + let messages: string[] = []; + + collectMessages(response?.data, messages); + + if (isRecord(error)) { + collectMessages(error.data, messages); + } + + if (messages.length > 0) { + return messages.join(' - '); + } + + if (error instanceof Error && error.message) { + return error.message; + } + + return 'Unknown error'; +}; + +let upstreamStatus = (error: unknown) => { + let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined; + return ( + response?.status ?? + (isRecord(error) && typeof error.status === 'number' ? error.status : undefined) + ); +}; + +export let sternFinancialDataServiceError = (message: string) => + new ServiceError(badRequestError({ message })); + +export let sternFinancialDataApiError = (error: unknown, operation = 'request') => { + if (error instanceof ServiceError) { + return error; + } + + let response = isRecord(error) ? (error.response as ErrorResponse | undefined) : undefined; + let status = upstreamStatus(error); + let statusText = response?.statusText; + let statusLabel = + status !== undefined ? `HTTP ${status}${statusText ? ` ${statusText}` : ''}: ` : ''; + + let serviceError = sternFinancialDataServiceError( + `Stern Financial Data ${operation} failed: ${statusLabel}${extractMessage(error)}` + ); + + serviceError.data.reason = 'stern_financial_data_error'; + serviceError.data.upstreamStatus = status; + + if (error instanceof Error) { + serviceError.setParent(error); + } + + return serviceError; +}; diff --git a/integrations/stern-financial-data/src/lib/extractor.test.ts b/integrations/stern-financial-data/src/lib/extractor.test.ts new file mode 100644 index 0000000000..daede40229 --- /dev/null +++ b/integrations/stern-financial-data/src/lib/extractor.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from 'vitest'; +import { extractRowsForSource, findHeaderMap, parseTypedValue } from './extractor'; +import { BETA_FIELDS, BetaRow, ERP_FIELDS, ErpRow } from './sources'; + +describe('Stern financial data extractor', () => { + it('parses typed values and header aliases', () => { + expect(parseTypedValue('4.23%', undefined, 'number')).toBe(0.0423); + expect(parseTypedValue('1,207', undefined, 'integer')).toBe(1207); + expect(parseTypedValue('NA', undefined, 'number')).toBeNull(); + + expect( + findHeaderMap( + [ + 'Country', + "Moody's rating", + 'Adj. Default Spread', + 'Country Risk Premium', + 'Equity Risk Premium', + 'Corporate Tax Rate', + 'Sovereignn CDS', + 'ERP based on sovereign CDSS' + ], + ERP_FIELDS + ) + ).toEqual({ + country: 0, + moodysRating: 1, + adjustedDefaultSpread: 2, + countryRiskPremium: 3, + equityRiskPremium: 4, + corporateTaxRate: 5, + sovereignCds: 6, + erpBasedOnSovereignCds: 7 + }); + }); + + it('parses ERP rows through the HTML fallback path', () => { + let rows = extractRowsForSource( + 'erp', + null, + '' + + "" + + '' + + '
CountryMoody's ratingAdj. Default SpreadCountry Risk PremiumEquity Risk PremiumCorporate Tax RateSovereignn CDSERP based on sovereign CDSS
United StatesAa10.23%0.23%4.46%25.00%0.30%4.69%
' + ) as ErpRow[]; + + expect(rows).toHaveLength(1); + expect(rows[0]).toMatchObject({ + country: 'United States', + moodysRating: 'Aa1', + adjustedDefaultSpread: 0.0023, + countryRiskPremium: 0.0023, + equityRiskPremium: 0.0446, + corporateTaxRate: 0.25, + sovereignCds: 0.003, + erpBasedOnSovereignCds: 0.0469 + }); + expect(rows[0]?.raw.adjustedDefaultSpread).toBe('0.23%'); + }); + + it('parses US industry beta rows through the HTML fallback path', () => { + let rows = extractRowsForSource( + 'us_industry_betas', + null, + '' + + '' + + '' + + '' + + '
Industry NameNumber of firmsBetaD/E RatioEffective Tax rateUnlevered betaCash/Firm valueUnlevered beta corrected for cashHiLo RiskStandard deviation of equityStandard deviation in operating income (last 10 years)
Advertising521.2140.20%5.02%0.937.73%1.010.623362.91%15.17%
Total Market5,9941.0451.48%13.31%0.759.70%0.830.369142.07%24.29%
' + ) as BetaRow[]; + + expect(rows).toHaveLength(2); + expect(rows[0]).toMatchObject({ + industryName: 'Advertising', + numberOfFirms: 52, + beta: 1.21, + debtToEquityRatio: 0.402, + effectiveTaxRate: 0.0502, + rowType: 'industry' + }); + expect(rows[1]).toMatchObject({ + industryName: 'Total Market', + numberOfFirms: 5994, + rowType: 'aggregate' + }); + }); + + it('parses global industry beta rows through the HTML fallback path', () => { + let rows = extractRowsForSource( + 'global_industry_betas', + null, + '
not the target table
' + + '' + + '' + + '' + + '' + + '
Industry NameNumber of firmsBetaD/E RatioEffective Tax rateUnlevered betaCash/Firm valueUnlevered beta corrected for cashHiLo RiskStandard deviation of equityStandard deviation in operating income (last 10 years)
Semiconductor Equip3912.1310.55%14.40%1.9412.00%2.200.567858.11%26.22%
Total Market (without financials)43,0561.0825.95%13.13%0.905.52%0.960.376543.02%24.86%
' + ) as BetaRow[]; + + expect(rows).toHaveLength(2); + expect(rows[0]).toMatchObject({ + industryName: 'Semiconductor Equip', + numberOfFirms: 391, + beta: 2.13, + debtToEquityRatio: 0.1055, + rowType: 'industry' + }); + expect(rows[1]).toMatchObject({ + industryName: 'Total Market (without financials)', + rowType: 'aggregate' + }); + }); +}); diff --git a/integrations/stern-financial-data/src/lib/extractor.ts b/integrations/stern-financial-data/src/lib/extractor.ts new file mode 100644 index 0000000000..c4e5208e18 --- /dev/null +++ b/integrations/stern-financial-data/src/lib/extractor.ts @@ -0,0 +1,718 @@ +import * as domino from '@mixmark-io/domino'; +import * as XLSX from 'xlsx'; +import { sternFinancialDataServiceError } from './errors'; +import { + BETA_FIELDS, + BetaField, + BetaRow, + ERP_FIELDS, + ErpField, + ErpRow, + SOURCES, + SourceId, + SternRow +} from './sources'; + +type Cell = { + text: string; + value?: unknown; +}; + +type TypedValueKind = 'string' | 'integer' | 'number'; +type HeaderMap = Record; +type Workbook = XLSX.WorkBook; +type Mapper = ( + cells: Cell[], + headerMap: HeaderMap +) => TRow | null; + +let HEADER_ALIASES: Record = { + country: ['country'], + moodysRating: ["moody's rating", 'moodys rating', 'moody rating'], + adjustedDefaultSpread: [ + 'adj. default spread', + 'adj default spread', + 'adjusted default spread', + 'rating-based default spread', + 'rating based default spread', + 'default spread' + ], + countryRiskPremium: ['country risk premium', 'crp'], + equityRiskPremium: ['equity risk premium', 'total equity risk premium', 'erp', 'final erp'], + corporateTaxRate: ['corporate tax rate', 'tax rate'], + sovereignCds: [ + 'sovereign cds', + 'sovereignn cds', + 'sovereign cds, net of swiss cds', + 'sovereign cds net of swiss cds' + ], + erpBasedOnSovereignCds: [ + 'erp based on sovereign cds', + 'erp based on sovereign cdss', + 'total equity risk premium2' + ], + industryName: ['industry name'], + numberOfFirms: ['number of firms'], + beta: ['beta'], + debtToEquityRatio: ['d/e ratio', 'de ratio', 'debt/equity ratio', 'debt to equity ratio'], + effectiveTaxRate: ['effective tax rate'], + unleveredBeta: ['unlevered beta'], + cashToFirmValue: ['cash/firm value', 'cash firm value'], + unleveredBetaCorrectedForCash: ['unlevered beta corrected for cash'], + hiloRisk: ['hilo risk', 'hi lo risk'], + standardDeviationOfEquity: [ + 'standard deviation of equity', + 'standard deviation equity', + 'standard deviation (equity)' + ], + standardDeviationInOperatingIncomeLast10Years: [ + 'standard deviation in operating income (last 10 years)', + 'standard deviation in operating income last 10 years', + 'standard deviation operating income' + ] +}; + +let UNAVAILABLE_VALUES = new Set(['', 'na', 'n/a', 'n.a.', 'nm', 'unavailable', '-', '—']); + +export let normalizeText = (value: unknown) => { + if (value === null || value === undefined) { + return ''; + } + + return String(value) + .replace(/\u00a0/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +}; + +export let normalizeHeader = (value: unknown) => + normalizeText(value) + .toLowerCase() + .replace(/&/g, ' and ') + .replace(/[’']/g, '') + .replace(/[^a-z0-9]+/g, ' ') + .replace(/\s+/g, ' ') + .trim(); + +let isUnavailable = (value: unknown) => + UNAVAILABLE_VALUES.has(normalizeText(value).toLowerCase()); + +let coerceNumberText = (value: unknown) => + normalizeText(value).replace(/[%,$]/g, '').replace(/,/g, '').replace(/[()]/g, '').trim(); + +let cleanNumber = (value: number) => + Number.isFinite(value) ? Number(value.toFixed(12)) : null; + +export let parseTypedValue = ( + text: unknown, + rawValue: unknown, + kind: TypedValueKind +): string | number | null => { + let normalized = normalizeText(text); + + if (isUnavailable(normalized)) { + return null; + } + + if (kind === 'string') { + return normalized || null; + } + + if (kind === 'integer') { + if (typeof rawValue === 'number' && !/%/.test(normalized)) { + return Math.round(rawValue); + } + + let intValue = Number.parseInt(coerceNumberText(normalized), 10); + return Number.isFinite(intValue) ? intValue : null; + } + + if (/%/.test(normalized)) { + let percentValue = Number.parseFloat(coerceNumberText(normalized)); + return cleanNumber(percentValue / 100); + } + + if (typeof rawValue === 'number') { + return cleanNumber(rawValue); + } + + let numberValue = Number.parseFloat(coerceNumberText(normalized)); + return cleanNumber(numberValue); +}; + +let parseStringValue = (text: unknown, rawValue: unknown) => + parseTypedValue(text, rawValue, 'string') as string | null; + +let parseNumberValue = (text: unknown, rawValue: unknown) => + parseTypedValue(text, rawValue, 'number') as number | null; + +let parseIntegerValue = (text: unknown, rawValue: unknown) => + parseTypedValue(text, rawValue, 'integer') as number | null; + +let compileAliases = (field: string) => + (HEADER_ALIASES[field] ?? [field]).map(normalizeHeader); + +let headerMatchesField = (header: string, field: string) => + compileAliases(field).includes(normalizeHeader(header)); + +export let findHeaderMap = ( + headers: readonly string[], + requiredFields: readonly T[] +) => { + let map: Partial> = {}; + + headers.forEach((header, index) => { + for (let field of requiredFields) { + if (map[field] === undefined && headerMatchesField(header, field)) { + map[field] = index; + } + } + }); + + let missing = requiredFields.filter(field => map[field] === undefined); + return missing.length > 0 ? null : (map as HeaderMap); +}; + +let hasAnyContent = (cells: readonly Cell[]) => cells.some(cell => normalizeText(cell.text)); + +let trimTrailingEmptyCells = (cells: Cell[]) => { + let end = cells.length; + + while (end > 0 && !normalizeText(cells[end - 1]?.text)) { + end -= 1; + } + + return cells.slice(0, end); +}; + +let cellAt = (cells: readonly Cell[], index: number) => + index >= 0 ? cells[index] : undefined; + +let getRawValue = (cells: readonly Cell[], index: number) => cellAt(cells, index)?.value; + +let makeRaw = ( + fields: readonly T[], + cells: readonly Cell[], + headerMap: HeaderMap +) => + fields.reduce( + (raw, field) => { + raw[field] = normalizeText(cellAt(cells, headerMap[field])?.text); + return raw; + }, + {} as Record + ); + +let mapErpRow = (cells: Cell[], headerMap: HeaderMap): ErpRow | null => { + let raw = makeRaw(ERP_FIELDS, cells, headerMap); + let country = raw.country; + + if (!country || /^country$/i.test(country) || /^frontier markets/i.test(country)) { + return null; + } + + return { + country: parseStringValue(raw.country, getRawValue(cells, headerMap.country)) ?? country, + moodysRating: parseStringValue( + raw.moodysRating, + getRawValue(cells, headerMap.moodysRating) + ), + adjustedDefaultSpread: parseNumberValue( + raw.adjustedDefaultSpread, + getRawValue(cells, headerMap.adjustedDefaultSpread) + ), + countryRiskPremium: parseNumberValue( + raw.countryRiskPremium, + getRawValue(cells, headerMap.countryRiskPremium) + ), + equityRiskPremium: parseNumberValue( + raw.equityRiskPremium, + getRawValue(cells, headerMap.equityRiskPremium) + ), + corporateTaxRate: parseNumberValue( + raw.corporateTaxRate, + getRawValue(cells, headerMap.corporateTaxRate) + ), + sovereignCds: parseNumberValue( + raw.sovereignCds, + getRawValue(cells, headerMap.sovereignCds) + ), + erpBasedOnSovereignCds: parseNumberValue( + raw.erpBasedOnSovereignCds, + getRawValue(cells, headerMap.erpBasedOnSovereignCds) + ), + raw + }; +}; + +export let classifyBetaRow = (industryName: string) => + /^total market/i.test(normalizeText(industryName)) ? 'aggregate' : 'industry'; + +let mapBetaRow = (cells: Cell[], headerMap: HeaderMap): BetaRow | null => { + let raw = makeRaw(BETA_FIELDS, cells, headerMap); + let industryName = raw.industryName; + + if (!industryName || /^industry name$/i.test(industryName)) { + return null; + } + + return { + industryName: + parseStringValue(raw.industryName, getRawValue(cells, headerMap.industryName)) ?? + industryName, + numberOfFirms: parseIntegerValue( + raw.numberOfFirms, + getRawValue(cells, headerMap.numberOfFirms) + ), + beta: parseNumberValue(raw.beta, getRawValue(cells, headerMap.beta)), + debtToEquityRatio: parseNumberValue( + raw.debtToEquityRatio, + getRawValue(cells, headerMap.debtToEquityRatio) + ), + effectiveTaxRate: parseNumberValue( + raw.effectiveTaxRate, + getRawValue(cells, headerMap.effectiveTaxRate) + ), + unleveredBeta: parseNumberValue( + raw.unleveredBeta, + getRawValue(cells, headerMap.unleveredBeta) + ), + cashToFirmValue: parseNumberValue( + raw.cashToFirmValue, + getRawValue(cells, headerMap.cashToFirmValue) + ), + unleveredBetaCorrectedForCash: parseNumberValue( + raw.unleveredBetaCorrectedForCash, + getRawValue(cells, headerMap.unleveredBetaCorrectedForCash) + ), + hiloRisk: parseNumberValue(raw.hiloRisk, getRawValue(cells, headerMap.hiloRisk)), + standardDeviationOfEquity: parseNumberValue( + raw.standardDeviationOfEquity, + getRawValue(cells, headerMap.standardDeviationOfEquity) + ), + standardDeviationInOperatingIncomeLast10Years: parseNumberValue( + raw.standardDeviationInOperatingIncomeLast10Years, + getRawValue(cells, headerMap.standardDeviationInOperatingIncomeLast10Years) + ), + rowType: classifyBetaRow(industryName), + raw + }; +}; + +let makeCell = (text: unknown, value?: unknown): Cell => ({ + text: normalizeText(text), + value +}); + +let sheetRows = (sheet: XLSX.WorkSheet | undefined) => { + if (!sheet || !sheet['!ref']) { + return []; + } + + let range = XLSX.utils.decode_range(sheet['!ref']); + let rows: Cell[][] = []; + + for (let rowIndex = range.s.r; rowIndex <= range.e.r; rowIndex += 1) { + let cells: Cell[] = []; + + for (let colIndex = range.s.c; colIndex <= range.e.c; colIndex += 1) { + let address = XLSX.utils.encode_cell({ r: rowIndex, c: colIndex }); + let cell = sheet[address]; + + cells.push(makeCell(cell ? (cell.w !== undefined ? cell.w : cell.v) : '', cell?.v)); + } + + cells = trimTrailingEmptyCells(cells); + + if (hasAnyContent(cells)) { + rows.push(cells); + } + } + + return rows; +}; + +export let parseWorkbook = (buffer: Buffer) => + XLSX.read(buffer, { type: 'buffer', cellDates: true, cellNF: true }); + +let findTableInRows = ( + rows: Cell[][], + requiredFields: readonly T[] +): { rows: Cell[][]; headerIndex: number; headerMap: HeaderMap } | null => { + for (let index = 0; index < rows.length; index += 1) { + let headers = rows[index]?.map(cell => cell.text) ?? []; + let headerMap = findHeaderMap(headers, requiredFields); + + if (headerMap) { + return { + rows, + headerIndex: index, + headerMap + }; + } + } + + return null; +}; + +let findWorkbookTable = ( + workbook: Workbook, + requiredFields: readonly T[] +) => { + for (let sheetName of workbook.SheetNames) { + let rows = sheetRows(workbook.Sheets[sheetName]); + let table = findTableInRows(rows, requiredFields); + + if (table) { + return { + ...table, + sheetName + }; + } + } + + return null; +}; + +let parseRowsAfterHeader = ( + rows: Cell[][], + headerIndex: number, + headerMap: HeaderMap, + mapper: Mapper +) => { + let parsed: TRow[] = []; + + for (let index = headerIndex + 1; index < rows.length; index += 1) { + let cells = rows[index] ?? []; + let firstCell = normalizeText(cells[0]?.text); + + if (!firstCell) { + break; + } + + let row = mapper(cells, headerMap); + + if (row) { + parsed.push(row); + } + } + + return parsed; +}; + +export let extractBetaRowsFromWorkbook = (workbook: Workbook) => { + let table = findWorkbookTable(workbook, BETA_FIELDS); + + if (!table) { + throw sternFinancialDataServiceError('Could not find beta table in workbook.'); + } + + return parseRowsAfterHeader(table.rows, table.headerIndex, table.headerMap, mapBetaRow); +}; + +let extractErpCountryRowsFromWorkbook = (workbook: Workbook) => { + let required = [ + 'country', + 'moodysRating', + 'adjustedDefaultSpread', + 'countryRiskPremium', + 'equityRiskPremium', + 'sovereignCds', + 'erpBasedOnSovereignCds' + ] as const; + let table = findWorkbookTable(workbook, required); + + if (!table) { + throw sternFinancialDataServiceError('Could not find country ERP table in workbook.'); + } + + let headerMap = { + ...table.headerMap, + corporateTaxRate: -1 + } satisfies HeaderMap; + + let rows: ErpRow[] = []; + + for (let index = table.headerIndex + 1; index < table.rows.length; index += 1) { + let cells = table.rows[index] ?? []; + let country = normalizeText(cellAt(cells, headerMap.country)?.text); + + if (!country || /^frontier markets/i.test(country)) { + break; + } + + let row = mapErpRow(cells, headerMap); + + if (row) { + rows.push(row); + } + } + + return rows; +}; + +let findFrontierTable = (workbook: Workbook) => { + for (let sheetName of workbook.SheetNames) { + let rows = sheetRows(workbook.Sheets[sheetName]); + + for (let index = 0; index < rows.length; index += 1) { + let headers = rows[index]?.map(cell => cell.text) ?? []; + let normalizedHeaders = headers.map(normalizeHeader); + let prsIndex = normalizedHeaders.indexOf('prs composite risk score'); + + if (prsIndex === -1) { + continue; + } + + let headerMap = findHeaderMap(headers, [ + 'country', + 'equityRiskPremium', + 'countryRiskPremium', + 'adjustedDefaultSpread' + ] as const); + + if (headerMap) { + return { + rows, + headerIndex: index, + headerMap + }; + } + } + } + + return null; +}; + +let extractErpFrontierRowsFromWorkbook = (workbook: Workbook) => { + let table = findFrontierTable(workbook); + + if (!table) { + return []; + } + + let rows: ErpRow[] = []; + + for (let index = table.headerIndex + 1; index < table.rows.length; index += 1) { + let cells = table.rows[index] ?? []; + let country = normalizeText(cellAt(cells, table.headerMap.country)?.text); + + if (!country) { + break; + } + + let adjustedDefaultSpreadCell = cellAt(cells, table.headerMap.adjustedDefaultSpread); + let countryRiskPremiumCell = cellAt(cells, table.headerMap.countryRiskPremium); + let equityRiskPremiumCell = cellAt(cells, table.headerMap.equityRiskPremium); + + rows.push({ + country, + moodysRating: 'NR', + adjustedDefaultSpread: parseNumberValue( + adjustedDefaultSpreadCell?.text, + adjustedDefaultSpreadCell?.value + ), + countryRiskPremium: parseNumberValue( + countryRiskPremiumCell?.text, + countryRiskPremiumCell?.value + ), + equityRiskPremium: parseNumberValue( + equityRiskPremiumCell?.text, + equityRiskPremiumCell?.value + ), + corporateTaxRate: null, + sovereignCds: null, + erpBasedOnSovereignCds: null, + raw: { + country, + moodysRating: 'NR', + adjustedDefaultSpread: normalizeText(adjustedDefaultSpreadCell?.text), + countryRiskPremium: normalizeText(countryRiskPremiumCell?.text), + equityRiskPremium: normalizeText(equityRiskPremiumCell?.text), + corporateTaxRate: '', + sovereignCds: '', + erpBasedOnSovereignCds: '' + } + }); + } + + return rows; +}; + +let extractCountryTaxRatesFromWorkbook = (workbook: Workbook) => { + let taxByCountry: Record = {}; + let sheet = workbook.Sheets['PRS Worksheet'] ?? workbook.Sheets['Country Tax Rates']; + + if (!sheet) { + return taxByCountry; + } + + let rows = sheetRows(sheet); + let headerMap: HeaderMap<'country' | 'corporateTaxRate'> | null = null; + let headerIndex = -1; + + for (let index = 0; index < rows.length; index += 1) { + headerMap = findHeaderMap(rows[index]?.map(cell => cell.text) ?? [], [ + 'country', + 'corporateTaxRate' + ] as const); + + if (headerMap) { + headerIndex = index; + break; + } + } + + if (!headerMap) { + return taxByCountry; + } + + for (let rowIndex = headerIndex + 1; rowIndex < rows.length; rowIndex += 1) { + let cells = rows[rowIndex] ?? []; + let countryCell = cellAt(cells, headerMap.country); + let taxCell = cellAt(cells, headerMap.corporateTaxRate); + let country = normalizeText(countryCell?.text); + + if (!country) { + continue; + } + + taxByCountry[country] = taxCell ?? makeCell('', undefined); + } + + return taxByCountry; +}; + +export let extractErpRowsFromWorkbook = (workbook: Workbook) => { + let erpRows = extractErpCountryRowsFromWorkbook(workbook); + let frontierRows = extractErpFrontierRowsFromWorkbook(workbook); + let taxByCountry = extractCountryTaxRatesFromWorkbook(workbook); + let rows = erpRows.concat(frontierRows).map(row => { + let tax = taxByCountry[row.country]; + + if (tax) { + row.corporateTaxRate = parseNumberValue(tax.text, tax.value); + row.raw.corporateTaxRate = normalizeText(tax.text); + } + + return row; + }); + + if (rows.length === 0) { + throw sternFinancialDataServiceError('Could not find ERP rows in workbook.'); + } + + return rows; +}; + +let removeNode = (node: any) => { + if (node?.parentNode) { + node.parentNode.removeChild(node); + } +}; + +let removeComments = (node: any) => { + let child = node?.firstChild; + + while (child) { + let next = child.nextSibling; + + if (child.nodeType === 8) { + removeNode(child); + } else { + removeComments(child); + } + + child = next; + } +}; + +let isHidden = (element: any) => { + let current = element; + + while (current?.getAttribute) { + let style = String(current.getAttribute('style') ?? '').toLowerCase(); + + if ( + current.hasAttribute?.('hidden') || + /display\s*:\s*none/.test(style) || + /visibility\s*:\s*hidden/.test(style) + ) { + return true; + } + + current = current.parentNode; + } + + return false; +}; + +let toArray = (list: ArrayLike | null | undefined) => Array.from(list ?? []); + +let cleanDocument = (html: string) => { + let window = domino.createWindow(html); + let document = window.document; + + toArray( + document.querySelectorAll('script, style, noscript, link, meta, title, colgroup, col') + ).forEach(removeNode); + removeComments(document); + + return document; +}; + +let tableRowsFromHtml = (html: string) => { + let document = cleanDocument(html); + let tables = toArray(document.querySelectorAll('table')); + + return tables.map((table: any) => + toArray(table.querySelectorAll('tr')) + .filter((row: any) => !isHidden(row)) + .map((row: any) => + trimTrailingEmptyCells( + toArray(row.querySelectorAll('th,td')) + .filter((cell: any) => !isHidden(cell)) + .map((cell: any) => makeCell(cell.textContent, undefined)) + ) + ) + .filter(hasAnyContent) + ); +}; + +let extractRowsFromHtml = ( + html: string, + requiredFields: readonly TField[], + mapper: Mapper +) => { + let tables = tableRowsFromHtml(html); + + for (let tableRows of tables) { + let table = findTableInRows(tableRows, requiredFields); + + if (table) { + return parseRowsAfterHeader(table.rows, table.headerIndex, table.headerMap, mapper); + } + } + + throw sternFinancialDataServiceError('Could not find matching table in HTML.'); +}; + +export let extractErpRowsFromHtml = (html: string) => + extractRowsFromHtml(html, ERP_FIELDS, mapErpRow); + +export let extractBetaRowsFromHtml = (html: string) => + extractRowsFromHtml(html, BETA_FIELDS, mapBetaRow); + +export let extractRowsForSource = ( + sourceId: SourceId, + workbook: Workbook | null, + html: string +) => { + let source = SOURCES[sourceId]; + + if (source.kind === 'erp') { + return workbook ? extractErpRowsFromWorkbook(workbook) : extractErpRowsFromHtml(html); + } + + return workbook ? extractBetaRowsFromWorkbook(workbook) : extractBetaRowsFromHtml(html); +}; diff --git a/integrations/stern-financial-data/src/lib/filters.ts b/integrations/stern-financial-data/src/lib/filters.ts new file mode 100644 index 0000000000..4892d6602a --- /dev/null +++ b/integrations/stern-financial-data/src/lib/filters.ts @@ -0,0 +1,128 @@ +import { BetaRow, BetaRowType, ErpRow, SourceId, SternRow } from './sources'; + +export type CommonControls = { + limit: number; + offset: number; + returnAll: boolean; + includeRaw: boolean; +}; + +export type ErpFilters = CommonControls & { + source: 'erp'; + countries?: string[]; + countrySearch?: string; + moodysRatings?: string[]; + minEquityRiskPremium?: number; + maxEquityRiskPremium?: number; + minCountryRiskPremium?: number; + maxCountryRiskPremium?: number; + minCorporateTaxRate?: number; + maxCorporateTaxRate?: number; + hasSovereignCds?: boolean; +}; + +export type BetaFilters = CommonControls & { + source: 'us_industry_betas' | 'global_industry_betas'; + industries?: string[]; + industrySearch?: string; + rowType?: BetaRowType; + minBeta?: number; + maxBeta?: number; + minUnleveredBeta?: number; + maxUnleveredBeta?: number; + minNumberOfFirms?: number; + maxDebtToEquityRatio?: number; +}; + +export type SourceFilters = ErpFilters | BetaFilters; + +export type PaginatedRows = { + filteredRows: SternRow[]; + returnedRows: Array | SternRow>; + returnedRowCount: number; + filteredRowCount: number; + totalRowCount: number; + truncated: boolean; +}; + +let normalizeMatchText = (value: string) => value.trim().toLowerCase(); + +let valueInList = (value: string | null | undefined, accepted: string[] | undefined) => { + if (!accepted?.length) return true; + if (!value) return false; + + let normalizedValue = normalizeMatchText(value); + return accepted.some(entry => normalizeMatchText(entry) === normalizedValue); +}; + +let valueContains = (value: string | null | undefined, search: string | undefined) => { + if (!search) return true; + if (!value) return false; + + return normalizeMatchText(value).includes(normalizeMatchText(search)); +}; + +let numberAtLeast = (value: number | null, threshold: number | undefined) => + threshold === undefined || (value !== null && value >= threshold); + +let numberAtMost = (value: number | null, threshold: number | undefined) => + threshold === undefined || (value !== null && value <= threshold); + +let isErpRow = (row: SternRow): row is ErpRow => 'country' in row; +let isBetaRow = (row: SternRow): row is BetaRow => 'industryName' in row; + +let filterErpRow = (row: ErpRow, filters: ErpFilters) => + valueInList(row.country, filters.countries) && + valueContains(row.country, filters.countrySearch) && + valueInList(row.moodysRating, filters.moodysRatings) && + numberAtLeast(row.equityRiskPremium, filters.minEquityRiskPremium) && + numberAtMost(row.equityRiskPremium, filters.maxEquityRiskPremium) && + numberAtLeast(row.countryRiskPremium, filters.minCountryRiskPremium) && + numberAtMost(row.countryRiskPremium, filters.maxCountryRiskPremium) && + numberAtLeast(row.corporateTaxRate, filters.minCorporateTaxRate) && + numberAtMost(row.corporateTaxRate, filters.maxCorporateTaxRate) && + (filters.hasSovereignCds === undefined || + (filters.hasSovereignCds ? row.sovereignCds !== null : row.sovereignCds === null)); + +let filterBetaRow = (row: BetaRow, filters: BetaFilters) => + valueInList(row.industryName, filters.industries) && + valueContains(row.industryName, filters.industrySearch) && + (filters.rowType === undefined || row.rowType === filters.rowType) && + numberAtLeast(row.beta, filters.minBeta) && + numberAtMost(row.beta, filters.maxBeta) && + numberAtLeast(row.unleveredBeta, filters.minUnleveredBeta) && + numberAtMost(row.unleveredBeta, filters.maxUnleveredBeta) && + numberAtLeast(row.numberOfFirms, filters.minNumberOfFirms) && + numberAtMost(row.debtToEquityRatio, filters.maxDebtToEquityRatio); + +let withoutRaw = (row: T) => { + let { raw, ...rest } = row; + return rest; +}; + +let shapeRows = (rows: SternRow[], includeRaw: boolean) => + includeRaw ? rows : rows.map(row => withoutRaw(row)); + +export let applySourceFilters = ( + sourceId: SourceId, + rows: SternRow[], + filters: SourceFilters +): PaginatedRows => { + let filteredRows = + sourceId === 'erp' + ? rows.filter(row => isErpRow(row) && filterErpRow(row, filters as ErpFilters)) + : rows.filter(row => isBetaRow(row) && filterBetaRow(row, filters as BetaFilters)); + + let returnedRows = filters.returnAll + ? filteredRows.slice(filters.offset) + : filteredRows.slice(filters.offset, filters.offset + filters.limit); + + return { + filteredRows, + returnedRows: shapeRows(returnedRows, filters.includeRaw), + returnedRowCount: returnedRows.length, + filteredRowCount: filteredRows.length, + totalRowCount: rows.length, + truncated: filters.offset + returnedRows.length < filteredRows.length + }; +}; diff --git a/integrations/stern-financial-data/src/lib/sources.ts b/integrations/stern-financial-data/src/lib/sources.ts new file mode 100644 index 0000000000..e9f80d9435 --- /dev/null +++ b/integrations/stern-financial-data/src/lib/sources.ts @@ -0,0 +1,148 @@ +export let PAGE_BASE = 'https://pages.stern.nyu.edu/~adamodar/New_Home_Page/datafile/'; +export let WORKBOOK_BASE = 'https://www.stern.nyu.edu/~adamodar/pc/datasets/'; + +export let ERP_FIELDS = [ + 'country', + 'moodysRating', + 'adjustedDefaultSpread', + 'countryRiskPremium', + 'equityRiskPremium', + 'corporateTaxRate', + 'sovereignCds', + 'erpBasedOnSovereignCds' +] as const; + +export let BETA_FIELDS = [ + 'industryName', + 'numberOfFirms', + 'beta', + 'debtToEquityRatio', + 'effectiveTaxRate', + 'unleveredBeta', + 'cashToFirmValue', + 'unleveredBetaCorrectedForCash', + 'hiloRisk', + 'standardDeviationOfEquity', + 'standardDeviationInOperatingIncomeLast10Years' +] as const; + +export type ErpField = (typeof ERP_FIELDS)[number]; +export type BetaField = (typeof BETA_FIELDS)[number]; +export type SourceId = 'erp' | 'us_industry_betas' | 'global_industry_betas'; +export type SourceKind = 'erp' | 'beta'; +export type SourceType = 'workbook' | 'html'; +export type BetaRowType = 'industry' | 'aggregate'; + +export type ErpRow = { + country: string; + moodysRating: string | null; + adjustedDefaultSpread: number | null; + countryRiskPremium: number | null; + equityRiskPremium: number | null; + corporateTaxRate: number | null; + sovereignCds: number | null; + erpBasedOnSovereignCds: number | null; + raw: Record; +}; + +export type BetaRow = { + industryName: string; + numberOfFirms: number | null; + beta: number | null; + debtToEquityRatio: number | null; + effectiveTaxRate: number | null; + unleveredBeta: number | null; + cashToFirmValue: number | null; + unleveredBetaCorrectedForCash: number | null; + hiloRisk: number | null; + standardDeviationOfEquity: number | null; + standardDeviationInOperatingIncomeLast10Years: number | null; + rowType: BetaRowType; + raw: Record; +}; + +export type SternRow = ErpRow | BetaRow; + +export type SternSource = { + id: SourceId; + title: string; + description: string; + kind: SourceKind; + pageUrl: string; + workbookUrl: string; + rowFields: readonly string[]; + supportedFilters: readonly string[]; +}; + +export let SOURCES: Record = { + erp: { + id: 'erp', + title: 'Country Equity Risk Premiums', + description: + "Country equity risk premiums, Moody's ratings, default spreads, tax rates, and sovereign CDS data.", + kind: 'erp', + pageUrl: `${PAGE_BASE}ctryprem.html`, + workbookUrl: `${WORKBOOK_BASE}ctryprem.xlsx`, + rowFields: ERP_FIELDS, + supportedFilters: [ + 'countries', + 'countrySearch', + 'moodysRatings', + 'minEquityRiskPremium', + 'maxEquityRiskPremium', + 'minCountryRiskPremium', + 'maxCountryRiskPremium', + 'minCorporateTaxRate', + 'maxCorporateTaxRate', + 'hasSovereignCds' + ] + }, + us_industry_betas: { + id: 'us_industry_betas', + title: 'US Industry Betas', + description: + 'US industry beta, leverage, tax, cash, risk, and volatility measures by industry.', + kind: 'beta', + pageUrl: `${PAGE_BASE}Betas.html`, + workbookUrl: `${WORKBOOK_BASE}betas.xls`, + rowFields: [...BETA_FIELDS, 'rowType'], + supportedFilters: [ + 'industries', + 'industrySearch', + 'rowType', + 'minBeta', + 'maxBeta', + 'minUnleveredBeta', + 'maxUnleveredBeta', + 'minNumberOfFirms', + 'maxDebtToEquityRatio' + ] + }, + global_industry_betas: { + id: 'global_industry_betas', + title: 'Global Industry Betas', + description: + 'Global industry beta, leverage, tax, cash, risk, and volatility measures by industry.', + kind: 'beta', + pageUrl: `${PAGE_BASE}BetasGlobal.html`, + workbookUrl: `${WORKBOOK_BASE}betaGlobal.xls`, + rowFields: [...BETA_FIELDS, 'rowType'], + supportedFilters: [ + 'industries', + 'industrySearch', + 'rowType', + 'minBeta', + 'maxBeta', + 'minUnleveredBeta', + 'maxUnleveredBeta', + 'minNumberOfFirms', + 'maxDebtToEquityRatio' + ] + } +}; + +export let SOURCE_IDS = Object.keys(SOURCES) as SourceId[]; +export let SOURCE_LIST = SOURCE_IDS.map(sourceId => SOURCES[sourceId]); + +export let isSourceId = (value: string): value is SourceId => + SOURCE_IDS.includes(value as SourceId); diff --git a/integrations/stern-financial-data/src/spec.ts b/integrations/stern-financial-data/src/spec.ts new file mode 100644 index 0000000000..97339da7b4 --- /dev/null +++ b/integrations/stern-financial-data/src/spec.ts @@ -0,0 +1,13 @@ +import { SlateSpecification } from 'slates'; +import { auth } from './auth'; +import { config } from './config'; + +export let spec = SlateSpecification.create({ + key: 'stern-financial-data', + name: 'Stern Financial Data', + description: + 'Query public NYU Stern financial datasets including country equity risk premiums and industry betas.', + metadata: {}, + config, + auth +}); diff --git a/integrations/stern-financial-data/src/tools/get-source.ts b/integrations/stern-financial-data/src/tools/get-source.ts new file mode 100644 index 0000000000..38fbf9f2d8 --- /dev/null +++ b/integrations/stern-financial-data/src/tools/get-source.ts @@ -0,0 +1,220 @@ +import { SlateTool } from 'slates'; +import { z } from 'zod'; +import { SternFinancialDataClient } from '../lib/client'; +import { applySourceFilters, BetaFilters, ErpFilters } from '../lib/filters'; +import { sternFinancialDataServiceError } from '../lib/errors'; +import { spec } from '../spec'; + +let commonControls = { + limit: z + .number() + .int() + .min(1) + .max(500) + .optional() + .default(25) + .describe('Maximum rows to return when returnAll is false. Defaults to 25.'), + offset: z + .number() + .int() + .min(0) + .optional() + .default(0) + .describe('Number of filtered rows to skip.'), + returnAll: z + .boolean() + .optional() + .default(false) + .describe('Return all filtered rows. Use sparingly because full Stern rows are wide.'), + includeRaw: z + .boolean() + .optional() + .default(false) + .describe('Include original cell text in each returned row.') +}; + +let percentageDescription = 'Decimal percentage value; for example, 0.05 means 5%.'; + +let erpInputSchema = z.object({ + source: z.literal('erp').describe('Country equity risk premium source.'), + ...commonControls, + countries: z + .array(z.string()) + .optional() + .describe('Exact country names to include, case-insensitive.'), + countrySearch: z + .string() + .optional() + .describe('Case-insensitive substring search over country names.'), + moodysRatings: z + .array(z.string()) + .optional() + .describe("Moody's ratings to include, such as Aaa, Baa2, Caa1, or NR."), + minEquityRiskPremium: z + .number() + .optional() + .describe(`Minimum equity risk premium. ${percentageDescription}`), + maxEquityRiskPremium: z + .number() + .optional() + .describe(`Maximum equity risk premium. ${percentageDescription}`), + minCountryRiskPremium: z + .number() + .optional() + .describe(`Minimum country risk premium. ${percentageDescription}`), + maxCountryRiskPremium: z + .number() + .optional() + .describe(`Maximum country risk premium. ${percentageDescription}`), + minCorporateTaxRate: z + .number() + .optional() + .describe(`Minimum corporate tax rate. ${percentageDescription}`), + maxCorporateTaxRate: z + .number() + .optional() + .describe(`Maximum corporate tax rate. ${percentageDescription}`), + hasSovereignCds: z + .boolean() + .optional() + .describe('Filter by whether the row has a sovereign CDS value.') +}); + +let betaFilterControls = { + industries: z + .array(z.string()) + .optional() + .describe('Exact industry names to include, case-insensitive.'), + industrySearch: z + .string() + .optional() + .describe('Case-insensitive substring search over industry names.'), + rowType: z + .enum(['industry', 'aggregate']) + .optional() + .describe('Filter regular industries or total-market aggregate rows.'), + minBeta: z.number().optional().describe('Minimum levered beta.'), + maxBeta: z.number().optional().describe('Maximum levered beta.'), + minUnleveredBeta: z.number().optional().describe('Minimum unlevered beta.'), + maxUnleveredBeta: z.number().optional().describe('Maximum unlevered beta.'), + minNumberOfFirms: z + .number() + .int() + .min(0) + .optional() + .describe('Minimum number of firms in the industry row.'), + maxDebtToEquityRatio: z + .number() + .optional() + .describe(`Maximum debt-to-equity ratio. ${percentageDescription}`) +}; + +let usIndustryBetasInputSchema = z.object({ + source: z.literal('us_industry_betas').describe('US industry beta source.'), + ...commonControls, + ...betaFilterControls +}); + +let globalIndustryBetasInputSchema = z.object({ + source: z.literal('global_industry_betas').describe('Global industry beta source.'), + ...commonControls, + ...betaFilterControls +}); + +let inputSchema = z.discriminatedUnion('source', [ + erpInputSchema, + usIndustryBetasInputSchema, + globalIndustryBetasInputSchema +]); + +let outputSchema = z.object({ + metadata: z.object({ + source: z.string().describe('Source id that was retrieved.'), + title: z.string().describe('Source title.'), + pageUrl: z.string().describe('Stern HTML page URL.'), + workbookUrl: z.string().describe('Stern workbook URL.'), + retrievedAt: z.string().describe('ISO timestamp for this retrieval.'), + sourceType: z + .enum(['workbook', 'html']) + .describe('Whether rows came from the workbook or HTML fallback.'), + workbookFallbackReason: z + .string() + .optional() + .describe('Reason workbook extraction fell back to HTML, when applicable.') + }), + totalRowCount: z.number().describe('Rows extracted before filtering.'), + filteredRowCount: z.number().describe('Rows remaining after filters.'), + returnedRowCount: z.number().describe('Rows returned in this response.'), + offset: z.number().describe('Filtered-row offset applied.'), + limit: z.number().nullable().describe('Row limit applied, or null when returnAll is true.'), + returnAll: z.boolean().describe('Whether all filtered rows were requested.'), + truncated: z.boolean().describe('Whether additional filtered rows were omitted.'), + rows: z.array(z.any()).describe('Filtered Stern financial data rows.') +}); + +let validateRange = (min: number | undefined, max: number | undefined, label: string) => { + if (min !== undefined && max !== undefined && min > max) { + throw sternFinancialDataServiceError(`${label} minimum cannot be greater than maximum.`); + } +}; + +let validateInput = (input: z.infer) => { + if (input.source === 'erp') { + validateRange(input.minEquityRiskPremium, input.maxEquityRiskPremium, 'equityRiskPremium'); + validateRange( + input.minCountryRiskPremium, + input.maxCountryRiskPremium, + 'countryRiskPremium' + ); + validateRange(input.minCorporateTaxRate, input.maxCorporateTaxRate, 'corporateTaxRate'); + return; + } + + validateRange(input.minBeta, input.maxBeta, 'beta'); + validateRange(input.minUnleveredBeta, input.maxUnleveredBeta, 'unleveredBeta'); +}; + +export let getSource = SlateTool.create(spec, { + name: 'Get Source', + key: 'get_source', + description: + 'Retrieve a Stern financial data source and return filtered rows. Full output is available with returnAll=true, but use filters and limits when possible because full rows include many financial metrics and raw cell text can be large.', + instructions: [ + 'Call list_sources first when you need the available source ids, row fields, or filter hints.', + 'Use filters such as countrySearch, industrySearch, rating, beta, premium, and rowType before requesting full output.', + 'Set includeRaw only when original formatted cell text is needed for audit or display.' + ], + tags: { + readOnly: true, + destructive: false + } +}) + .input(inputSchema) + .output(outputSchema) + .handleInvocation(async ctx => { + validateInput(ctx.input); + + let client = new SternFinancialDataClient(); + let result = await client.getSource(ctx.input.source); + let paginated = applySourceFilters( + ctx.input.source, + result.rows, + ctx.input.source === 'erp' ? (ctx.input as ErpFilters) : (ctx.input as BetaFilters) + ); + + return { + output: { + metadata: result.metadata, + totalRowCount: paginated.totalRowCount, + filteredRowCount: paginated.filteredRowCount, + returnedRowCount: paginated.returnedRowCount, + offset: ctx.input.offset, + limit: ctx.input.returnAll ? null : ctx.input.limit, + returnAll: ctx.input.returnAll, + truncated: paginated.truncated, + rows: paginated.returnedRows + }, + message: `Retrieved **${paginated.returnedRowCount}** of **${paginated.filteredRowCount}** filtered Stern **${ctx.input.source}** rows.` + }; + }) + .build(); diff --git a/integrations/stern-financial-data/src/tools/index.ts b/integrations/stern-financial-data/src/tools/index.ts new file mode 100644 index 0000000000..f91ede2bc4 --- /dev/null +++ b/integrations/stern-financial-data/src/tools/index.ts @@ -0,0 +1,2 @@ +export * from './get-source'; +export * from './list-sources'; diff --git a/integrations/stern-financial-data/src/tools/list-sources.ts b/integrations/stern-financial-data/src/tools/list-sources.ts new file mode 100644 index 0000000000..511582689a --- /dev/null +++ b/integrations/stern-financial-data/src/tools/list-sources.ts @@ -0,0 +1,50 @@ +import { SlateTool } from 'slates'; +import { z } from 'zod'; +import { SOURCE_LIST } from '../lib/sources'; +import { spec } from '../spec'; + +export let listSources = SlateTool.create(spec, { + name: 'List Sources', + key: 'list_sources', + description: + 'List the NYU Stern financial data sources supported by this integration, including fields and filter hints.', + tags: { + readOnly: true, + destructive: false + } +}) + .input(z.object({})) + .output( + z.object({ + sources: z + .array( + z.object({ + id: z.string().describe('Source id to pass to get_source.'), + title: z.string().describe('Human-readable source title.'), + description: z.string().describe('Source contents.'), + pageUrl: z.string().describe('Stern HTML page URL.'), + workbookUrl: z.string().describe('Stern workbook URL.'), + rowFields: z.array(z.string()).describe('Fields returned for source rows.'), + supportedFilters: z + .array(z.string()) + .describe('Filter inputs supported by get_source.') + }) + ) + .describe('Supported Stern financial data sources.') + }) + ) + .handleInvocation(async () => ({ + output: { + sources: SOURCE_LIST.map(source => ({ + id: source.id, + title: source.title, + description: source.description, + pageUrl: source.pageUrl, + workbookUrl: source.workbookUrl, + rowFields: [...source.rowFields], + supportedFilters: [...source.supportedFilters] + })) + }, + message: `Found **${SOURCE_LIST.length}** Stern financial data sources.` + })) + .build(); diff --git a/integrations/stern-financial-data/src/types.d.ts b/integrations/stern-financial-data/src/types.d.ts new file mode 100644 index 0000000000..22037b15b9 --- /dev/null +++ b/integrations/stern-financial-data/src/types.d.ts @@ -0,0 +1,3 @@ +declare module '@mixmark-io/domino' { + export function createWindow(html: string): any; +} diff --git a/integrations/stern-financial-data/tsconfig.json b/integrations/stern-financial-data/tsconfig.json new file mode 100644 index 0000000000..2abe727831 --- /dev/null +++ b/integrations/stern-financial-data/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "types": ["node"], + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "moduleResolution": "bundler", + + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "include": ["src"] +} diff --git a/integrations/stern-financial-data/vitest.config.ts b/integrations/stern-financial-data/vitest.config.ts new file mode 100644 index 0000000000..db5407c09a --- /dev/null +++ b/integrations/stern-financial-data/vitest.config.ts @@ -0,0 +1,7 @@ +import { createSlatesVitestConfig } from '@slates/test/config'; + +export default createSlatesVitestConfig({ + test: { + include: ['src/**/*.test.ts', 'src/**/*.e2e.ts'] + } +}); diff --git a/packages/slates/example/google-sheets/auth.ts b/packages/slates/example/google-sheets/auth.ts index 58801439ed..6d3060c07f 100644 --- a/packages/slates/example/google-sheets/auth.ts +++ b/packages/slates/example/google-sheets/auth.ts @@ -1,6 +1,22 @@ import { createAxios, SlateAuth } from 'slates'; import { z } from 'zod'; +type GoogleOauthInput = Record; + +type GoogleOauthOutput = { + token: string; + refreshToken?: string; + expiresAt?: string; +}; + +type GoogleOauthRefreshContext = { + output: GoogleOauthOutput; + input: GoogleOauthInput; + clientId: string; + clientSecret: string; + scopes: string[]; +}; + let axios = createAxios({ baseURL: 'https://oauth2.googleapis.com' }); @@ -85,7 +101,7 @@ export let auth = SlateAuth.create() }; }, - handleTokenRefresh: async ctx => { + handleTokenRefresh: async (ctx: GoogleOauthRefreshContext) => { if (!ctx.output.refreshToken) { throw new Error('No refresh token available'); }