diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 091a425..125a28e 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -27,40 +27,3 @@ jobs: working-directory: sitio - run: pnpm run lint working-directory: sitio - - oci-sitio: - name: "[amd64] oci:sitio" - needs: check-sitio - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - lfs: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/sitio - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: docker/sitio.Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/sitio:buildcache - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/sitio:buildcache,mode=max - platforms: linux/amd64 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d076e8b..eb090d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,6 +202,9 @@ importers: '@types/pg': specifier: ^8.15.4 version: 8.15.4 + '@vitest/ui': + specifier: ^3.2.4 + version: 3.2.4(vitest@3.2.4) autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.5.1) @@ -262,6 +265,9 @@ importers: vite: specifier: ^6.1.0 version: 6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@20.17.17)(@vitest/ui@3.2.4)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) twitter-bot: dependencies: @@ -1982,12 +1988,18 @@ packages: '@types/better-sqlite3@7.6.13': resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/connect@3.4.36': resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -2045,6 +2057,40 @@ packages: '@types/ws@8.5.14': resolution: {integrity: sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==} + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/ui@3.2.4': + resolution: {integrity: sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==} + peerDependencies: + vitest: 3.2.4 + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -2120,6 +2166,10 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -2217,6 +2267,10 @@ packages: caniuse-lite@1.0.30001697: resolution: {integrity: sha512-GwNPlWJin8E+d7Gxq96jxM6w0w+VFeyyXRsjU58emtkYqnbwHqXm5uT2uCmO0RQE9htWknOP4xtBlLmM/gWxvQ==} + chai@5.2.1: + resolution: {integrity: sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2241,6 +2295,10 @@ packages: peerDependencies: chart.js: '>=3.0.0' + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2388,10 +2446,23 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decompress-response@6.0.0: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-extend@0.6.0: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} @@ -2581,6 +2652,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2632,6 +2706,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -2662,6 +2739,10 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} + engines: {node: '>=12.0.0'} + fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -2677,6 +2758,14 @@ packages: picomatch: optional: true + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -2684,6 +2773,9 @@ packages: fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + fflate@0.8.2: + resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -2695,6 +2787,9 @@ packages: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + foreground-child@3.3.0: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} @@ -2938,6 +3033,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -3021,6 +3119,9 @@ packages: long@5.2.4: resolution: {integrity: sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==} + loupe@3.1.4: + resolution: {integrity: sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -3303,6 +3404,13 @@ packages: pathe@2.0.2: resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pg-cloudflare@1.2.7: resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} @@ -3664,6 +3772,9 @@ packages: shimmer@1.2.1: resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -3681,6 +3792,10 @@ packages: resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} engines: {node: '>=18'} + sirv@3.0.1: + resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} + engines: {node: '>=18'} + slug@6.1.0: resolution: {integrity: sha512-x6vLHCMasg4DR2LPiyFGI0gJJhywY6DTiGhCrOMzb3SOk/0JVLIaL4UhyFSHu04SD3uAavrKY/K3zZ3i6iRcgA==} @@ -3719,6 +3834,12 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -3750,6 +3871,9 @@ packages: resolution: {integrity: sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==} engines: {node: '>=14.16'} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + style-to-object@1.0.8: resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} @@ -3793,6 +3917,7 @@ packages: svelte-mq-store@2.2.22: resolution: {integrity: sha512-O611Iww9WcwTxTTlPFDeOK9xuwht3C0Ug8gVGvfpQQLV8F///cg86qroBWgb/RP0j8C2q1ridgWoAtCqnLUBgQ==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. peerDependencies: '@sveltejs/kit': ^2.0.0 svelte: ^4.0.0 @@ -3871,6 +3996,9 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} @@ -3878,6 +4006,22 @@ packages: resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4025,6 +4169,11 @@ packages: resolution: {integrity: sha512-OOvVnaBTqJJ2J7X1cM1qpH4pj9jsfTxia1VSuWeyXtf+OnP8d0YI1LHpv8y2NT47wg+n7XiTgh3BvcSffuBWrw==} engines: {node: '>=18.0.0'} + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite@6.1.0: resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -4073,6 +4222,34 @@ packages: vite: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} @@ -4111,6 +4288,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -5573,7 +5755,7 @@ snapshots: dependencies: '@sentry/browser': 8.54.0 '@sentry/core': 8.54.0 - magic-string: 0.30.7 + magic-string: 0.30.17 svelte: 5.20.1 '@sentry/sveltekit@8.54.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.28.0)(@sveltejs/kit@2.17.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.1)(vite@6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0)))(svelte@5.20.1)(vite@6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0)))(svelte@5.20.1)(vite@6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0))': @@ -5836,12 +6018,18 @@ snapshots: dependencies: '@types/node': 20.17.17 + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + '@types/connect@3.4.36': dependencies: '@types/node': 20.17.17 '@types/cookie@0.6.0': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.6': {} '@types/linkify-it@3.0.5': {} @@ -5911,6 +6099,59 @@ snapshots: '@types/node': 20.17.17 optional: true + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.0.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.17 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.3 + + '@vitest/ui@3.2.4(vitest@3.2.4)': + dependencies: + '@vitest/utils': 3.2.4 + fflate: 0.8.2 + flatted: 3.3.3 + pathe: 2.0.3 + sirv: 3.0.1 + tinyglobby: 0.2.14 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/node@20.17.17)(@vitest/ui@3.2.4)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.1.4 + tinyrainbow: 2.0.0 + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -5975,6 +6216,8 @@ snapshots: aria-query@5.3.2: {} + assertion-error@2.0.1: {} + ast-types@0.16.1: dependencies: tslib: 2.8.1 @@ -6084,6 +6327,14 @@ snapshots: caniuse-lite@1.0.30001697: {} + chai@5.2.1: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.4 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6104,6 +6355,8 @@ snapshots: dependencies: chart.js: 4.4.7 + check-error@2.1.1: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -6236,10 +6489,16 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.1: + dependencies: + ms: 2.1.3 + decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 + deep-eql@5.0.2: {} + deep-extend@0.6.0: {} deepmerge@4.3.1: {} @@ -6358,6 +6617,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -6489,6 +6750,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + event-target-shim@5.0.1: {} eventemitter3@5.0.1: {} @@ -6521,6 +6786,8 @@ snapshots: expand-template@2.0.3: {} + expect-type@1.2.2: {} + fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6537,6 +6804,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 @@ -6545,6 +6816,8 @@ snapshots: fflate@0.4.8: {} + fflate@0.8.2: {} + file-uri-to-path@1.0.0: {} fill-range@7.1.1: @@ -6556,6 +6829,8 @@ snapshots: locate-path: 6.0.0 path-exists: 4.0.0 + flatted@3.3.3: {} + foreground-child@3.3.0: dependencies: cross-spawn: 7.0.6 @@ -6778,6 +7053,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@4.1.0: dependencies: argparse: 2.0.1 @@ -6865,6 +7142,8 @@ snapshots: long@5.2.4: {} + loupe@3.1.4: {} + lru-cache@10.4.3: {} lru-cache@5.1.1: @@ -7096,6 +7375,10 @@ snapshots: pathe@2.0.2: {} + pathe@2.0.3: {} + + pathval@2.0.1: {} + pg-cloudflare@1.2.7: optional: true @@ -7445,6 +7728,8 @@ snapshots: shimmer@1.2.1: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} simple-concat@1.0.1: {} @@ -7465,6 +7750,12 @@ snapshots: mrmime: 2.0.0 totalist: 3.0.1 + sirv@3.0.1: + dependencies: + '@polka/url': 1.0.0-next.28 + mrmime: 2.0.0 + totalist: 3.0.1 + slug@6.1.0: {} smol-toml@1.3.1: {} @@ -7509,6 +7800,10 @@ snapshots: split2@4.2.0: optional: true + stackback@0.0.2: {} + + std-env@3.9.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -7539,6 +7834,10 @@ snapshots: strip-json-comments@5.0.1: {} + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + style-to-object@1.0.8: dependencies: inline-style-parser: 0.2.4 @@ -7703,6 +8002,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} + tinyexec@0.3.2: {} tinyglobby@0.2.10: @@ -7710,6 +8011,17 @@ snapshots: fdir: 6.4.3(picomatch@4.0.2) picomatch: 4.0.2 + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.3: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -7856,6 +8168,27 @@ snapshots: transitivePeerDependencies: - rollup + vite-node@3.2.4(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite@6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0): dependencies: esbuild: 0.24.2 @@ -7872,6 +8205,48 @@ snapshots: optionalDependencies: vite: 6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + vitest@3.2.4(@types/node@20.17.17)(@vitest/ui@3.2.4)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.1 + debug: 4.4.1 + expect-type: 1.2.2 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.1.0(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + vite-node: 3.2.4(@types/node@20.17.17)(jiti@2.4.2)(tsx@4.19.2)(yaml@2.7.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.17.17 + '@vitest/ui': 3.2.4(vitest@3.2.4) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + wcwidth@1.0.1: dependencies: defaults: 1.0.4 @@ -7907,6 +8282,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 diff --git a/sitio/drizzle/0001_clean_micromacro.sql b/sitio/drizzle/0001_clean_micromacro.sql new file mode 100644 index 0000000..2910daf --- /dev/null +++ b/sitio/drizzle/0001_clean_micromacro.sql @@ -0,0 +1 @@ +ALTER TABLE "db_scraps" ADD CONSTRAINT "db_scraps_uid_unique" UNIQUE("uid"); \ No newline at end of file diff --git a/sitio/drizzle/meta/0001_snapshot.json b/sitio/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..e1ff628 --- /dev/null +++ b/sitio/drizzle/meta/0001_snapshot.json @@ -0,0 +1,354 @@ +{ + "id": "c2e27cff-3bf5-46b6-ad9f-de8f9098aefc", + "prevId": "c19e9290-6bed-4226-921b-e3e4f9d00b1b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.db_cuentas": { + "name": "db_cuentas", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_data_json": { + "name": "account_data_json", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.db_historic_liked_tweets": { + "name": "db_historic_liked_tweets", + "schema": "", + "columns": { + "post_id": { + "name": "post_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "posted_at": { + "name": "posted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "estimated_liked_at": { + "name": "estimated_liked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.db_liked_tweets": { + "name": "db_liked_tweets", + "schema": "", + "columns": { + "url": { + "name": "url", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scrap_id": { + "name": "scrap_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "first_seen_at_idx": { + "name": "first_seen_at_idx", + "columns": [ + { + "expression": "first_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "liked_tweets_last_seen_at_idx": { + "name": "liked_tweets_last_seen_at_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "liked_tweets_scrap_id_idx": { + "name": "liked_tweets_scrap_id_idx", + "columns": [ + { + "expression": "scrap_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.db_retweets": { + "name": "db_retweets", + "schema": "", + "columns": { + "poster_id": { + "name": "poster_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "poster_handle": { + "name": "poster_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "post_id": { + "name": "post_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "retweet_at": { + "name": "retweet_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "posted_at": { + "name": "posted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scrap_id": { + "name": "scrap_id", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "retweet_at_idx": { + "name": "retweet_at_idx", + "columns": [ + { + "expression": "retweet_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "db_retweets_poster_id_post_id_pk": { + "name": "db_retweets_poster_id_post_id_pk", + "columns": [ + "poster_id", + "post_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.db_scraper_tokens": { + "name": "db_scraper_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.db_scraps": { + "name": "db_scraps", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "at": { + "name": "at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "cuenta_id": { + "name": "cuenta_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_tweets_seen": { + "name": "total_tweets_seen", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "db_scraps_finished_at_idx": { + "name": "db_scraps_finished_at_idx", + "columns": [ + { + "expression": "at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "db_scraps_uid_unique": { + "name": "db_scraps_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "uid" + ] + } + } + }, + "public.db_tweets": { + "name": "db_tweets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "twitter_scraper_json": { + "name": "twitter_scraper_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "captured_at": { + "name": "captured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/sitio/drizzle/meta/_journal.json b/sitio/drizzle/meta/_journal.json index c1f168f..10f889b 100644 --- a/sitio/drizzle/meta/_journal.json +++ b/sitio/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1751979572932, "tag": "0000_ordinary_tarantula", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1752015623244, + "tag": "0001_clean_micromacro", + "breakpoints": true } ] } \ No newline at end of file diff --git a/sitio/package.json b/sitio/package.json index 446c987..ad0d525 100644 --- a/sitio/package.json +++ b/sitio/package.json @@ -11,6 +11,9 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --check .", "format": "prettier --write .", + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui", "benchmark:lastWeek": "tsx ./benchmarks/lastWeek.ts", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", @@ -57,6 +60,7 @@ "@types/markdown-it": "^13.0.9", "@types/node": "^20.17.17", "@types/pg": "^8.15.4", + "@vitest/ui": "^3.2.4", "autoprefixer": "^10.4.20", "bits-ui": "1.0.0-next.98", "clsx": "^2.1.1", @@ -77,7 +81,8 @@ "tailwindcss-animate": "^1.0.7", "tsx": "^4.19.2", "typescript": "^5.7.3", - "vite": "^6.1.0" + "vite": "^6.1.0", + "vitest": "^3.2.4" }, "packageManager": "pnpm@9.15.5+sha256.8472168c3e1fd0bff287e694b053fccbbf20579a3ff9526b6333beab8df65a8d" -} +} \ No newline at end of file diff --git a/sitio/src/routes/api/internal/scraper/scrap/+server.ts b/sitio/src/routes/api/internal/scraper/scrap/+server.ts index 54e5f42..b3269d7 100644 --- a/sitio/src/routes/api/internal/scraper/scrap/+server.ts +++ b/sitio/src/routes/api/internal/scraper/scrap/+server.ts @@ -10,7 +10,7 @@ import { } from "../../../../../schema.js"; import { zScrap, type PostScrapRes } from "api/schema.js"; -export async function POST({ request }) { +export async function POST({ request }: { request: Request }) { { let authHeader = request.headers.get("Authorization"); const token = authHeader?.slice("Bearer ".length) || null; @@ -35,7 +35,7 @@ export async function POST({ request }) { totalTweetsSeen: scrap.totalTweetsSeen, }) .returning({ id: scraps.id }) - .onConflictDoNothing(); + .onConflictDoNothing({ target: scraps.uid }); let dbScrap: { id: number }; if (!x[0]) { const y = await tx.query.scraps.findFirst({ @@ -88,7 +88,7 @@ export async function POST({ request }) { ...values, }) .onConflictDoUpdate({ - target: [tweets.id], + target: tweets.id, set: values, where: gt(tweets.capturedAt, tweet.capturedAt), }); diff --git a/sitio/src/routes/api/internal/scraper/scrap/server.test.ts b/sitio/src/routes/api/internal/scraper/scrap/server.test.ts new file mode 100644 index 0000000..95898d7 --- /dev/null +++ b/sitio/src/routes/api/internal/scraper/scrap/server.test.ts @@ -0,0 +1,248 @@ +// Set up test database URL +const TEST_DATABASE_URL = "postgresql://localhost/milei_test"; + +// Override the DATABASE_URL environment variable for testing +process.env.DATABASE_URL = TEST_DATABASE_URL; + +import { + describe, + it, + expect, + beforeEach, + afterEach, + beforeAll, + afterAll, + vi, +} from "vitest"; +import type { RequestEvent } from "@sveltejs/kit"; +import { nanoid } from "nanoid"; +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "../../../../../schema.js"; +import { eq, sql } from "drizzle-orm"; + +// Create test database connection +const client = postgres(TEST_DATABASE_URL, { max: 1 }); +const testDb = drizzle(client, { schema }); + +// Import the endpoints - they should now use the test database due to env var +import { POST } from "./+server.js"; +import { GET as lastIdsGET } from "../last-ids/+server.js"; + +// Mock request/response for testing +function mockRequest( + body: any, + headers: Record = {}, +): RequestEvent { + return { + request: { + json: async () => body, + headers: { + get: (name: string) => headers[name] || null, + }, + }, + url: new URL("http://localhost/api/internal/scraper/scrap"), + platform: {}, + cookies: {} as any, + locals: {} as any, + params: {}, + route: {} as any, + setHeaders: () => {}, + getClientAddress: () => "127.0.0.1", + isDataRequest: false, + isSubRequest: false, + fetch: global.fetch, + } as any; +} + +// Test data +const validScrapData = { + uid: nanoid(), + finishedAt: new Date().toISOString(), + totalTweetsSeen: 100, + likedTweets: [ + { + url: "https://twitter.com/test/status/123", + firstSeenAt: new Date().toISOString(), + text: "Test tweet", + }, + ], + retweets: [ + { + posterId: "user123", + posterHandle: "testuser", + postId: "tweet123", + firstSeenAt: new Date().toISOString(), + retweetAt: new Date().toISOString(), + postedAt: new Date().toISOString(), + text: "Test retweet", + }, + ], + tweets: [ + { + id: "tweet456", + twitterScraperJson: JSON.stringify({ + id: "tweet456", + text: "Test tweet content", + user: { id: "user123", username: "testuser" }, + }), + capturedAt: new Date().toISOString(), + }, + ], +}; + +const validToken = "test-token-123"; + +describe("Scraper API Real Database Tests", () => { + beforeAll(async () => { + // Insert test token (schema is already created by migrations) + await testDb.insert(schema.scraperTokens).values({ + token: validToken, + }); + }); + + beforeEach(async () => { + // Clean up test data before each test + await testDb.delete(schema.tweets); + await testDb.delete(schema.retweets); + await testDb.delete(schema.likedTweets); + await testDb.delete(schema.scraps); + }); + + afterAll(async () => { + // Clean up test token + await testDb + .delete(schema.scraperTokens) + .where(eq(schema.scraperTokens.token, validToken)); + }); + + describe("POST /api/internal/scraper/scrap", () => { + it("should reject requests without authorization header", async () => { + const request = mockRequest(validScrapData); + + try { + await POST(request); + expect.fail("Expected POST to throw an error"); + } catch (error: any) { + expect(error.status).toBe(401); + expect(error.body.message).toContain("no Bearer token"); + } + }); + + it("should reject requests with invalid token", async () => { + const request = mockRequest(validScrapData, { + Authorization: "Bearer invalid-token", + }); + + try { + await POST(request); + expect.fail("Expected POST to throw an error"); + } catch (error: any) { + expect(error.status).toBe(401); + expect(error.body.message).toContain("invalid token"); + } + }); + + it("should reject requests with invalid scrap data", async () => { + const request = mockRequest( + { invalidField: "invalid" }, + { Authorization: `Bearer ${validToken}` }, + ); + + try { + await POST(request); + expect.fail("Expected POST to throw an error"); + } catch (error: any) { + expect(error.status).toBe(400); + } + }); + + it("should successfully process valid scrap data with PostgreSQL", async () => { + const request = mockRequest(validScrapData, { + Authorization: `Bearer ${validToken}`, + }); + + const response = await POST(request); + expect(response.status).toBe(200); + + const data = await response.json(); + expect(data).toHaveProperty("scrapId"); + expect(typeof data.scrapId).toBe("number"); + + // Verify data was inserted into PostgreSQL database + const scraps = await testDb.select().from(schema.scraps); + expect(scraps.length).toBe(1); + expect(scraps[0].uid).toBe(validScrapData.uid); + + const likedTweets = await testDb.select().from(schema.likedTweets); + expect(likedTweets.length).toBe(1); + expect(likedTweets[0].url).toBe(validScrapData.likedTweets[0].url); + + const retweets = await testDb.select().from(schema.retweets); + expect(retweets.length).toBe(1); + expect(retweets[0].posterId).toBe(validScrapData.retweets[0].posterId); + + const tweets = await testDb.select().from(schema.tweets); + expect(tweets.length).toBe(1); + expect(tweets[0].id).toBe(validScrapData.tweets[0].id); + }); + + it("should handle PostgreSQL conflict resolution correctly", async () => { + // First request + const request1 = mockRequest(validScrapData, { + Authorization: `Bearer ${validToken}`, + }); + + const response1 = await POST(request1); + expect(response1.status).toBe(200); + + // Second request with same UID should use PostgreSQL onConflictDoNothing + const request2 = mockRequest(validScrapData, { + Authorization: `Bearer ${validToken}`, + }); + + const response2 = await POST(request2); + expect(response2.status).toBe(200); + + // Should still only have one scrap record due to PostgreSQL unique constraint + const scraps = await testDb.select().from(schema.scraps); + expect(scraps.length).toBe(1); + }); + }); + + describe("GET /api/internal/scraper/last-ids", () => { + it("should return last tweet IDs from PostgreSQL", async () => { + // Insert test tweets into PostgreSQL + await testDb.insert(schema.tweets).values([ + { + id: "tweet1", + twitterScraperJson: { test: "data1" }, + capturedAt: new Date("2023-01-01"), + }, + { + id: "tweet2", + twitterScraperJson: { test: "data2" }, + capturedAt: new Date("2023-01-02"), + }, + ]); + + const response = await lastIdsGET(); + expect(response.status).toBe(200); + + const data = await response.json(); + expect(Array.isArray(data)).toBe(true); + expect(data.length).toBe(2); + expect(data).toContain("tweet1"); + expect(data).toContain("tweet2"); + }); + + it("should handle empty PostgreSQL results", async () => { + const response = await lastIdsGET(); + expect(response.status).toBe(200); + + const data = await response.json(); + expect(Array.isArray(data)).toBe(true); + expect(data).toEqual([]); + }); + }); +}); diff --git a/sitio/src/schema.test.ts b/sitio/src/schema.test.ts deleted file mode 100644 index cbdad30..0000000 --- a/sitio/src/schema.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -// import assert from "assert"; -// import { decodeIdArray, encodeIdArray } from "./schema"; - -// const origArray = ["1783452934785", "1823479128734", "9487309287"]; -// const buffer = encodeIdArray(origArray); -// const buffer2 = Buffer.from([ -// 0, 0, 1, 159, 62, 20, 34, 129, 0, 0, 1, 168, 143, 211, 98, 158, 0, 0, 0, 2, -// 53, 124, 217, 231, -// ]); -// assert(buffer.equals(buffer2)); - -// const array = decodeIdArray(buffer); -// assert( -// array.every((item, index) => origArray[index] === item) && -// array.length === origArray.length, -// ); diff --git a/sitio/src/schema.ts b/sitio/src/schema.ts index 748527c..e8c390f 100644 --- a/sitio/src/schema.ts +++ b/sitio/src/schema.ts @@ -45,7 +45,9 @@ export const historicLikedTweets = pgTable("db_historic_liked_tweets", { postId: text("post_id").primaryKey(), url: text("url").notNull(), postedAt: timestamp("posted_at", { withTimezone: true }).notNull(), - estimatedLikedAt: timestamp("estimated_liked_at", { withTimezone: true }).notNull(), + estimatedLikedAt: timestamp("estimated_liked_at", { + withTimezone: true, + }).notNull(), }); export const retweets = pgTable( @@ -104,7 +106,7 @@ export const scraps = pgTable( "db_scraps", { id: serial("id").primaryKey(), - uid: text("uid"), + uid: text("uid").unique(), finishedAt: timestamp("at", { withTimezone: true }).notNull(), cuentaId: text("cuenta_id"), totalTweetsSeen: integer("total_tweets_seen"), @@ -141,4 +143,4 @@ export const zTokenAccountData = z.object({ auth_token: z.string(), }); -export type TokenAccountData = z.infer; \ No newline at end of file +export type TokenAccountData = z.infer; diff --git a/sitio/vitest.config.ts b/sitio/vitest.config.ts new file mode 100644 index 0000000..ffb7278 --- /dev/null +++ b/sitio/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; +import { sveltekit } from '@sveltejs/kit/vite'; + +export default defineConfig({ + plugins: [sveltekit()], + test: { + environment: 'node', + }, +}); \ No newline at end of file