From b2bc36897f2b9512105eeb207196336655ef2f98 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 28 Aug 2024 00:13:39 +0100 Subject: [PATCH 01/43] refactor: prepare for v3 --- .eslintrc.json | 216 ----- .prettierrc.json | 8 - news.js | 7 - package.json | 37 - pnpm-lock.yaml | 1910 ----------------------------------------- src/handler.js | 91 -- src/index.js | 12 - src/pagination.js | 13 - src/router.js | 68 -- src/sizes.js | 6 - src/v2/index.js | 186 ---- src/v2/marketplace.js | 170 ---- src/v2/pexels.js | 24 - src/v2/sponsors.js | 16 - src/v2/unsplash.js | 37 - src/v2/weather.js | 24 - supabase.sql | 108 --- wrangler.toml | 36 - 18 files changed, 2969 deletions(-) delete mode 100644 .eslintrc.json delete mode 100644 .prettierrc.json delete mode 100644 news.js delete mode 100644 package.json delete mode 100644 pnpm-lock.yaml delete mode 100644 src/handler.js delete mode 100644 src/index.js delete mode 100644 src/pagination.js delete mode 100644 src/router.js delete mode 100644 src/sizes.js delete mode 100644 src/v2/index.js delete mode 100644 src/v2/marketplace.js delete mode 100644 src/v2/pexels.js delete mode 100644 src/v2/sponsors.js delete mode 100644 src/v2/unsplash.js delete mode 100644 src/v2/weather.js delete mode 100644 supabase.sql delete mode 100644 wrangler.toml diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 8ee8264..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,216 +0,0 @@ -{ - "env": { - "browser": true, - "commonjs": false, - "es6": true, - "node": false - }, - "extends": [ - "eslint:recommended" - ], - "parserOptions": { - "ecmaVersion": 12, - "sourceType": "module" - }, - "plugins": [], - "root": true, - "rules": { - "array-bracket-newline": [ - "error", - "consistent" - ], - "array-bracket-spacing": [ - "error", - "never" - ], - "array-element-newline": [ - "error", - "consistent" - ], - "arrow-body-style": [ - "error", - "as-needed" - ], - "arrow-parens": [ - "error", - "always" - ], - "block-spacing": [ - "error", - "always" - ], - "brace-style": [ - "error", - "1tbs" - ], - "comma-dangle": [ - "error", - { - "arrays": "always-multiline", - "exports": "always-multiline", - "functions": "always-multiline", - "imports": "always-multiline", - "objects": "always-multiline" - } - ], - "comma-spacing": [ - "error", - { - "after": true, - "before": false - } - ], - "comma-style": [ - "error", - "last" - ], - "computed-property-spacing": [ - "error", - "never" - ], - "curly": [ - "error", - "multi-line", // "multi" - "consistent" - ], - "default-case-last": [ - "error" - ], - "dot-location": [ - "error", - "property" - ], - "dot-notation": [ - "error" - ], - "eqeqeq": [ - "error" - ], - "func-call-spacing": [ - "error", - "never" - ], - "indent": [ - "error", - "tab" - ], - "linebreak-style": [ - "off", - "windows" - ], - "max-depth": [ - "warn", - { - "max": 5 - } - ], - "max-len": [ - "warn", - { - "code": 180, - "ignoreRegExpLiterals": true, - "ignoreStrings": true, - "ignoreTemplateLiterals": true, - "ignoreTrailingComments": true, - "ignoreUrls": true - } - ], - "max-lines": [ - "warn" - ], - "max-statements-per-line": [ - "error" - ], - "multiline-comment-style": [ - "off" - ], - "no-console": [ - "off" - ], - "no-prototype-builtins": [ - "off" - ], - "no-return-assign": [ - "error" - ], - "no-template-curly-in-string": [ - "warn" - ], - "no-trailing-spaces": [ - "error" - ], - "no-underscore-dangle": [ - "warn", - { - "allowAfterThis": true, - "allowFunctionParams": true - } - ], - "no-unneeded-ternary": [ - "error" - ], - "no-unused-expressions": [ - "error" - ], - "no-var": [ - "error" - ], - "no-whitespace-before-property": [ - "error" - ], - "object-curly-newline": [ - "error" - // { - // "minProperties": 3, - // "multiline": true - // } - ], - "object-curly-spacing": [ - "error", - "always" - ], - "object-property-newline": [ - "error" - ], - "operator-linebreak": [ - "error" - ], - "prefer-arrow-callback": [ - "error" - ], - "prefer-const": [ - "error", - { - "destructuring": "all", - "ignoreReadBeforeAssign": false - } - ], - "quotes": [ - "error", - "single" - ], - "rest-spread-spacing": [ - "error", - "never" - ], - "semi": [ - "error", - "always" - ], - "sort-keys": [ - "warn", - "asc", - { - "natural": true - } - ], - "space-in-parens": [ - "error", - "never" - ], - "spaced-comment": [ - "error", - "always" - ] - } -} \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index a14248e..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "printWidth": 100, - "trailingComma": "all", - "tabWidth": 2, - "useTabs": true, - "semi": true, - "singleQuote": true -} diff --git a/news.js b/news.js deleted file mode 100644 index 86c5b8a..0000000 --- a/news.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - date: '2022-07-20T21:14:38.000Z', - description: 'Mue 7.0 is now available!', - link: 'https://blog.muetab.com', - link_text: 'See what\'s new', - title: 'Mue 7.0', -}; \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index 492c8f5..0000000 --- a/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "mue-api", - "private": true, - "description": "API for Mue, a new tab extension.", - "repository": { - "url": "github:mue/api" - }, - "homepage": "https://api.muetab.com", - "author": "The Mue Authors (https://github.com/mue/api/graphs/contributors)", - "license": "MIT", - "main": "src/", - "version": "1.7.0", - "scripts": { - "deploy": "wrangler deploy", - "start": "wrangler dev", - "dev": "wrangler dev", - "test": "jest", - "pretty": "prettier --write \"./src/**/*.{js,json}\"" - }, - "dependencies": { - "@baselime/edge-logger": "^0.2.2", - "@microlabs/otel-cf-workers": "1.0.0-rc.32", - "@opentelemetry/api": "^1.8.0", - "@supabase/supabase-js": "^2.0.5", - "cheerio": "1.0.0-rc.12", - "itty-router": "^2.6.6", - "itty-router-extras": "^0.4.6", - "node-fetch": "2.6.7", - "ua-parser-js": "1.0.32" - }, - "devDependencies": { - "eslint": "^8.27.0", - "eslint-config-prettier": "^8.5.0", - "prettier": "^3.2.5", - "wrangler": "^3.48.0" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index 2eea875..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,1910 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - '@baselime/edge-logger': - specifier: ^0.2.2 - version: 0.2.2 - '@microlabs/otel-cf-workers': - specifier: 1.0.0-rc.32 - version: 1.0.0-rc.32 - '@opentelemetry/api': - specifier: ^1.8.0 - version: 1.8.0 - '@supabase/supabase-js': - specifier: ^2.0.5 - version: 2.0.5 - cheerio: - specifier: 1.0.0-rc.12 - version: 1.0.0-rc.12 - itty-router: - specifier: ^2.6.6 - version: 2.6.6 - itty-router-extras: - specifier: ^0.4.6 - version: 0.4.6 - node-fetch: - specifier: 2.6.7 - version: 2.6.7 - ua-parser-js: - specifier: 1.0.32 - version: 1.0.32 - -devDependencies: - eslint: - specifier: ^8.27.0 - version: 8.27.0 - eslint-config-prettier: - specifier: ^8.5.0 - version: 8.5.0(eslint@8.27.0) - prettier: - specifier: ^3.2.5 - version: 3.2.5 - wrangler: - specifier: ^3.48.0 - version: 3.48.0 - -packages: - - /@baselime/edge-logger@0.2.2: - resolution: {integrity: sha512-7i50PawqZDLFMzWcFKOoWZeb7AcuHdDwiiiXCUdFSjB3NkCJKO4ZxevUXFR3yL82ROSvrNDz83NEDpxyBx78eA==} - dev: false - - /@cloudflare/kv-asset-handler@0.3.1: - resolution: {integrity: sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA==} - dependencies: - mime: 3.0.0 - dev: true - - /@cloudflare/workerd-darwin-64@1.20240404.0: - resolution: {integrity: sha512-rc/ov3I9GwgKRtUnkShNW3TIoZEPHzExrMRNlHq1VpXQRBSchHdMw8meMn54+oqgxW1AKLmPWj/c0A7EnYAsIw==} - engines: {node: '>=16'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@cloudflare/workerd-darwin-arm64@1.20240404.0: - resolution: {integrity: sha512-V9oPjeC2PYBCoAYnjbO2bsjT7PtzxfUHnh780FUi1r59Hwxd7FNlojwsIidA0nS/1WV5UKeJusIdrYlQbsketA==} - engines: {node: '>=16'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@cloudflare/workerd-linux-64@1.20240404.0: - resolution: {integrity: sha512-ndO7q6G2X8uYd5byGFzow4SWPqINQcmJ7pKKATNa+9vh/YMO0of2ihPntnm9uv577l8nRiAwRkHbnsWoEI33qQ==} - engines: {node: '>=16'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@cloudflare/workerd-linux-arm64@1.20240404.0: - resolution: {integrity: sha512-hto5pjKYFqFu2Rvxnh84TzgDwalBRXQSwOVHltcgqo48en9TJDCN48ZtLj2G7QTGUOMW88h+nqdbj8+P7S/GXQ==} - engines: {node: '>=16'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@cloudflare/workerd-windows-64@1.20240404.0: - resolution: {integrity: sha512-DpCLvNkOeFinKGJwv9qbyT7RLZ1168dhPx85IHSqAYVWZIszNSmNOkEDqklvoJoab01AqETrrEhwBdmjCA7qfA==} - engines: {node: '>=16'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@cspotcode/source-map-support@0.8.1: - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - dev: true - - /@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19): - resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} - peerDependencies: - esbuild: '*' - dependencies: - esbuild: 0.17.19 - dev: true - - /@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19): - resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==} - peerDependencies: - esbuild: '*' - dependencies: - esbuild: 0.17.19 - escape-string-regexp: 4.0.0 - rollup-plugin-node-polyfills: 0.2.1 - dev: true - - /@esbuild/android-arm64@0.17.19: - resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-arm@0.17.19: - resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/android-x64@0.17.19: - resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-arm64@0.17.19: - resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/darwin-x64@0.17.19: - resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-arm64@0.17.19: - resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/freebsd-x64@0.17.19: - resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm64@0.17.19: - resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-arm@0.17.19: - resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ia32@0.17.19: - resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-loong64@0.17.19: - resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-mips64el@0.17.19: - resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-ppc64@0.17.19: - resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-riscv64@0.17.19: - resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-s390x@0.17.19: - resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/linux-x64@0.17.19: - resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true - optional: true - - /@esbuild/netbsd-x64@0.17.19: - resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/openbsd-x64@0.17.19: - resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true - optional: true - - /@esbuild/sunos-x64@0.17.19: - resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-arm64@0.17.19: - resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-ia32@0.17.19: - resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@esbuild/win32-x64@0.17.19: - resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true - optional: true - - /@eslint/eslintrc@1.3.3: - resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.4.1 - globals: 13.17.0 - ignore: 5.2.0 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@fastify/busboy@2.1.0: - resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} - engines: {node: '>=14'} - dev: true - - /@humanwhocodes/config-array@0.11.7: - resolution: {integrity: sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true - - /@humanwhocodes/object-schema@1.2.1: - resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} - dev: true - - /@jridgewell/resolve-uri@3.1.1: - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} - dev: true - - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true - - /@jridgewell/trace-mapping@0.3.9: - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true - - /@microlabs/otel-cf-workers@1.0.0-rc.32: - resolution: {integrity: sha512-G+sjxHPmw0ndqGvR7WdIY+61t7UhAUfZ+dv1WL/mji3vRpG67zHciXV4Hjibr34KRzGn0wQp9MsZVaQ9x2fWcQ==} - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/exporter-trace-otlp-http': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/otlp-exporter-base': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/otlp-transformer': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-trace-base': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.23.0 - ts-checked-fsm: 1.1.0 - dev: false - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.13.0 - dev: true - - /@opentelemetry/api-logs@0.43.0: - resolution: {integrity: sha512-0CXMOYPXgAdLM2OzVkiUfAL6QQwWVhnMfUXCqLsITY42FZ9TxAhZIHkoc4mfVxvPuXsBnRYGR8UQZX86p87z4A==} - engines: {node: '>=14'} - dependencies: - '@opentelemetry/api': 1.8.0 - dev: false - - /@opentelemetry/api@1.6.0: - resolution: {integrity: sha512-OWlrQAnWn9577PhVgqjUvMr1pg57Bc4jv0iL4w0PRuOSRvq67rvHW9Ie/dZVMvCzhSCB+UxhcY/PmCmFj33Q+g==} - engines: {node: '>=8.0.0'} - dev: false - - /@opentelemetry/api@1.8.0: - resolution: {integrity: sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==} - engines: {node: '>=8.0.0'} - dev: false - - /@opentelemetry/core@1.17.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-tfnl3h+UefCgx1aeN2xtrmr6BmdWGKXypk0pflQR0urFS40aE88trnkOMc2HTJZbMrqEEl4HsaBeFhwLVXsrJg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/semantic-conventions': 1.17.0 - dev: false - - /@opentelemetry/core@1.23.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-hdQ/a9TMzMQF/BO8Cz1juA43/L5YGtCSiKoOHmrTEf7VMDAZgy8ucpWx3eQTnQ3gBloRcWtzvcrMZABC3PTSKQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/semantic-conventions': 1.23.0 - dev: false - - /@opentelemetry/exporter-trace-otlp-http@0.43.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-X6RGl4RTWC13EBrFstAbTh4vKqVqf6afpvFcud9qYhvl2A53OZ5RTAQP+9MrAMhthiKQaftNsEDdB2/0Sq+Xkw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/otlp-exporter-base': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/otlp-transformer': 0.43.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-trace-base': 1.17.0(@opentelemetry/api@1.6.0) - dev: false - - /@opentelemetry/otlp-exporter-base@0.43.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-LXNtRFVuPRXB9q0qdvrLikQ3NtT9Jmv255Idryz3RJPhOh/Fa03sBASQoj3D55OH3xazmA90KFHfhJ/d8D8y4A==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.0.0 - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - dev: false - - /@opentelemetry/otlp-transformer@0.43.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-KXYmgzWdVBOD5NvPmGW1nEMJjyQ8gK3N8r6pi4HvmEhTp0v4T13qDSax4q0HfsqmbPJR355oqQSJUnu1dHNutw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/api-logs': 0.43.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-logs': 0.43.0(@opentelemetry/api-logs@0.43.0)(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-metrics': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/sdk-trace-base': 1.17.0(@opentelemetry/api@1.6.0) - dev: false - - /@opentelemetry/resources@1.17.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-+u0ciVnj8lhuL/qGRBPeVYvk7fL+H/vOddfvmOeJaA1KC+5/3UED1c9KoZQlRsNT5Kw1FaK8LkY2NVLYfOVZQw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.17.0 - dev: false - - /@opentelemetry/resources@1.23.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-iPRLfVfcEQynYGo7e4Di+ti+YQTAY0h5mQEUJcHlU9JOqpb4x965O6PZ+wMcwYVY63G96KtdS86YCM1BF1vQZg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.23.0 - dev: false - - /@opentelemetry/sdk-logs@0.43.0(@opentelemetry/api-logs@0.43.0)(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-JyJ2BBRKm37Mc4cSEhFmsMl5ASQn1dkGhEWzAAMSlhPtLRTv5PfvJwhR+Mboaic/eDLAlciwsgijq8IFlf6IgQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.4.0 <1.7.0' - '@opentelemetry/api-logs': '>=0.39.1' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/api-logs': 0.43.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - dev: false - - /@opentelemetry/sdk-metrics@1.17.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-HlWM27yGmYuwCoVRe3yg2PqKnIsq0kEF0HQgvkeDWz2NYkq9fFaSspR6kvjxUTbghAlZrabiqbgyKoYpYaXS3w==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.3.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - lodash.merge: 4.6.2 - dev: false - - /@opentelemetry/sdk-trace-base@1.17.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-2T5HA1/1iE36Q9eg6D4zYlC4Y4GcycI1J6NsHPKZY9oWfAxWsoYnRlkPfUqyY5XVtocCo/xHpnJvGNHwzT70oQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.7.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.17.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.17.0 - dev: false - - /@opentelemetry/sdk-trace-base@1.23.0(@opentelemetry/api@1.6.0): - resolution: {integrity: sha512-PzBmZM8hBomUqvCddF/5Olyyviayka44O5nDWq673np3ctnvwMOvNrsUORZjKja1zJbwEuD9niAGbnVrz3jwRQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.9.0' - dependencies: - '@opentelemetry/api': 1.6.0 - '@opentelemetry/core': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/resources': 1.23.0(@opentelemetry/api@1.6.0) - '@opentelemetry/semantic-conventions': 1.23.0 - dev: false - - /@opentelemetry/semantic-conventions@1.17.0: - resolution: {integrity: sha512-+fguCd2d8d2qruk0H0DsCEy2CTK3t0Tugg7MhZ/UQMvmewbZLNnJ6heSYyzIZWG5IPfAXzoj4f4F/qpM7l4VBA==} - engines: {node: '>=14'} - dev: false - - /@opentelemetry/semantic-conventions@1.23.0: - resolution: {integrity: sha512-MiqFvfOzfR31t8cc74CTP1OZfz7MbqpAnLCra8NqQoaHJX6ncIRTdYOQYBDQ2uFISDq0WY8Y9dDTWvsgzzBYRg==} - engines: {node: '>=14'} - dev: false - - /@supabase/functions-js@2.0.0: - resolution: {integrity: sha512-ozb7bds2yvf5k7NM2ZzUkxvsx4S4i2eRKFSJetdTADV91T65g4gCzEs9L3LUXSrghcGIkUaon03VPzOrFredqg==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - dev: false - - /@supabase/gotrue-js@2.2.3: - resolution: {integrity: sha512-2pw29sk2Fv/6CHFd72bIGTUNxI3UsyaEvjdpvgvvFtX7DmVBZPuotmQaZ56/V0sKREBdglt+bwIjOece1lmDCw==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - dev: false - - /@supabase/postgrest-js@1.1.0: - resolution: {integrity: sha512-qkY8TqIu5sJuae8gjeDPjEqPrefzcTraW9PNSVJQHq4TEv98ZmwaXGwBGz0bVL63bqrGA5hqREbQHkANUTXrvA==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - dev: false - - /@supabase/realtime-js@2.1.0: - resolution: {integrity: sha512-iplLCofTeYjnx9FIOsIwHLhMp0+7UVyiA4/sCeq40VdOgN9eTIhjEno9Tgh4dJARi4aaXoKfRX1DTxgZaOpPAw==} - dependencies: - '@types/phoenix': 1.5.4 - websocket: 1.0.34 - transitivePeerDependencies: - - supports-color - dev: false - - /@supabase/storage-js@2.0.0: - resolution: {integrity: sha512-7kXThdRt/xqnOOvZZxBqNkeX1CFNUWc0hYBJtNN/Uvt8ok9hD14foYmroWrHn046wEYFqUrB9U35JYsfTrvltA==} - dependencies: - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - dev: false - - /@supabase/supabase-js@2.0.5: - resolution: {integrity: sha512-zewk3U8wSe2UlKqzUk7iNxeMZzgDa5kjosSG4SKuAizIv+xzS+c2EZsYnNtc2byGjah4QT1dw3t91k2E+j+Akg==} - dependencies: - '@supabase/functions-js': 2.0.0 - '@supabase/gotrue-js': 2.2.3 - '@supabase/postgrest-js': 1.1.0 - '@supabase/realtime-js': 2.1.0 - '@supabase/storage-js': 2.0.0 - cross-fetch: 3.1.5 - transitivePeerDependencies: - - encoding - - supports-color - dev: false - - /@types/phoenix@1.5.4: - resolution: {integrity: sha512-L5eZmzw89eXBKkiqVBcJfU1QGx9y+wurRIEgt0cuLH0hwNtVUxtx+6cu0R2STwWj468sjXyBYPYDtGclUd1kjQ==} - dev: false - - /acorn-jsx@5.3.2(acorn@8.8.1): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.8.1 - dev: true - - /acorn-walk@8.3.1: - resolution: {integrity: sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==} - engines: {node: '>=0.4.0'} - dev: true - - /acorn@8.8.1: - resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: true - - /anymatch@3.1.2: - resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} - engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /as-table@1.0.55: - resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==} - dependencies: - printable-characters: 1.0.42 - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - dev: true - - /blake3-wasm@2.1.5: - resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} - dev: true - - /boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - dev: false - - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - - /bufferutil@4.0.7: - resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==} - engines: {node: '>=6.14.2'} - requiresBuild: true - dependencies: - node-gyp-build: 4.5.0 - dev: false - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true - - /capnp-ts@0.7.0: - resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} - dependencies: - debug: 4.3.4 - tslib: 2.6.2 - transitivePeerDependencies: - - supports-color - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /cheerio-select@2.1.0: - resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} - dependencies: - boolbase: 1.0.0 - css-select: 5.1.0 - css-what: 6.1.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.0.1 - dev: false - - /cheerio@1.0.0-rc.12: - resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} - engines: {node: '>= 6'} - dependencies: - cheerio-select: 2.1.0 - dom-serializer: 2.0.0 - domhandler: 5.0.3 - domutils: 3.0.1 - htmlparser2: 8.0.1 - parse5: 7.1.1 - parse5-htmlparser2-tree-adapter: 7.0.0 - dev: false - - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.2 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.2 - dev: true - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: true - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - - /cookie@0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} - engines: {node: '>= 0.6'} - dev: true - - /cross-fetch@3.1.5: - resolution: {integrity: sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==} - dependencies: - node-fetch: 2.6.7 - transitivePeerDependencies: - - encoding - dev: false - - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: true - - /css-select@5.1.0: - resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} - dependencies: - boolbase: 1.0.0 - css-what: 6.1.0 - domhandler: 5.0.3 - domutils: 3.0.1 - nth-check: 2.1.1 - dev: false - - /css-what@6.1.0: - resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} - engines: {node: '>= 6'} - dev: false - - /d@1.0.1: - resolution: {integrity: sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==} - dependencies: - es5-ext: 0.10.62 - type: 1.2.0 - dev: false - - /data-uri-to-buffer@2.0.2: - resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==} - dev: true - - /debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.0.0 - dev: false - - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true - - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - - /dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.4.0 - dev: false - - /domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - dev: false - - /domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - dependencies: - domelementtype: 2.3.0 - dev: false - - /domutils@3.0.1: - resolution: {integrity: sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==} - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dev: false - - /entities@4.4.0: - resolution: {integrity: sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==} - engines: {node: '>=0.12'} - dev: false - - /es5-ext@0.10.62: - resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==} - engines: {node: '>=0.10'} - requiresBuild: true - dependencies: - es6-iterator: 2.0.3 - es6-symbol: 3.1.3 - next-tick: 1.1.0 - dev: false - - /es6-iterator@2.0.3: - resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} - dependencies: - d: 1.0.1 - es5-ext: 0.10.62 - es6-symbol: 3.1.3 - dev: false - - /es6-symbol@3.1.3: - resolution: {integrity: sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==} - dependencies: - d: 1.0.1 - ext: 1.7.0 - dev: false - - /esbuild@0.17.19: - resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.17.19 - '@esbuild/android-arm64': 0.17.19 - '@esbuild/android-x64': 0.17.19 - '@esbuild/darwin-arm64': 0.17.19 - '@esbuild/darwin-x64': 0.17.19 - '@esbuild/freebsd-arm64': 0.17.19 - '@esbuild/freebsd-x64': 0.17.19 - '@esbuild/linux-arm': 0.17.19 - '@esbuild/linux-arm64': 0.17.19 - '@esbuild/linux-ia32': 0.17.19 - '@esbuild/linux-loong64': 0.17.19 - '@esbuild/linux-mips64el': 0.17.19 - '@esbuild/linux-ppc64': 0.17.19 - '@esbuild/linux-riscv64': 0.17.19 - '@esbuild/linux-s390x': 0.17.19 - '@esbuild/linux-x64': 0.17.19 - '@esbuild/netbsd-x64': 0.17.19 - '@esbuild/openbsd-x64': 0.17.19 - '@esbuild/sunos-x64': 0.17.19 - '@esbuild/win32-arm64': 0.17.19 - '@esbuild/win32-ia32': 0.17.19 - '@esbuild/win32-x64': 0.17.19 - dev: true - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true - - /eslint-config-prettier@8.5.0(eslint@8.27.0): - resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - dependencies: - eslint: 8.27.0 - dev: true - - /eslint-scope@7.1.1: - resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - - /eslint-utils@3.0.0(eslint@8.27.0): - resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} - engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} - peerDependencies: - eslint: '>=5' - dependencies: - eslint: 8.27.0 - eslint-visitor-keys: 2.1.0 - dev: true - - /eslint-visitor-keys@2.1.0: - resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} - engines: {node: '>=10'} - dev: true - - /eslint-visitor-keys@3.3.0: - resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /eslint@8.27.0: - resolution: {integrity: sha512-0y1bfG2ho7mty+SiILVf9PfuRA49ek4Nc60Wmmu62QlobNR+CeXa4xXIJgcuwSQgZiWaPH+5BDsctpIW0PR/wQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint/eslintrc': 1.3.3 - '@humanwhocodes/config-array': 0.11.7 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.1.1 - eslint-utils: 3.0.0(eslint@8.27.0) - eslint-visitor-keys: 3.3.0 - espree: 9.4.1 - esquery: 1.4.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.17.0 - grapheme-splitter: 1.0.4 - ignore: 5.2.0 - import-fresh: 3.3.0 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-sdsl: 4.1.5 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.1 - regexpp: 3.2.0 - strip-ansi: 6.0.1 - strip-json-comments: 3.1.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - - /espree@9.4.1: - resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.8.1 - acorn-jsx: 5.3.2(acorn@8.8.1) - eslint-visitor-keys: 3.3.0 - dev: true - - /esquery@1.4.0: - resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} - engines: {node: '>=0.10'} - dependencies: - estraverse: 5.3.0 - dev: true - - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - dependencies: - estraverse: 5.3.0 - dev: true - - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true - - /estree-walker@0.6.1: - resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} - dev: true - - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - - /exit-hook@2.2.1: - resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} - engines: {node: '>=6'} - dev: true - - /ext@1.7.0: - resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} - dependencies: - type: 2.7.2 - dev: false - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true - - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true - - /fastq@1.13.0: - resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} - dependencies: - reusify: 1.0.4 - dev: true - - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flat-cache: 3.0.4 - dev: true - - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - - /flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 - dev: true - - /flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - dev: true - - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true - optional: true - - /function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - dev: true - - /get-source@2.0.12: - resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} - dependencies: - data-uri-to-buffer: 2.0.2 - source-map: 0.6.1 - dev: true - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob-to-regexp@0.4.1: - resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} - dev: true - - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - - /globals@13.17.0: - resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - - /grapheme-splitter@1.0.4: - resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - dependencies: - function-bind: 1.1.2 - dev: true - - /htmlparser2@8.0.1: - resolution: {integrity: sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.0.1 - entities: 4.4.0 - dev: false - - /ignore@5.2.0: - resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} - engines: {node: '>= 4'} - dev: true - - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: true - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: true - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true - - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - dependencies: - binary-extensions: 2.2.0 - dev: true - - /is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - dependencies: - hasown: 2.0.2 - dev: true - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true - - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true - - /is-typedarray@1.0.0: - resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} - dev: false - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true - - /itty-router-extras@0.4.6: - resolution: {integrity: sha512-6r7HQBkFMPSJfcKksrKC7avEQnPCSSEvoz6PAAZMNhz8hthYu1pzedXDrvTFDWXJosfuaittzoNciWHO/TxMaw==} - dev: false - - /itty-router@2.6.6: - resolution: {integrity: sha512-hIPHtXGymCX7Lzb2I4G6JgZFE4QEEQwst9GORK7sMYUpJvLfy4yZJr95r04e8DzoAnj6HcxM2m4TbK+juu+18g==} - dev: false - - /js-sdsl@4.1.5: - resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==} - dev: true - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true - - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true - - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - /magic-string@0.25.9: - resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} - dependencies: - sourcemap-codec: 1.4.8 - dev: true - - /mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - dev: true - - /miniflare@3.20240404.0: - resolution: {integrity: sha512-+FOTcztPMW3akmucX4vE0PWMNvP4JBwl4s9ieA84fcOaDtTbtfU1rHXpcacj16klpUpvSnD6xd8Sjsn6SJXPfg==} - engines: {node: '>=16.13'} - hasBin: true - dependencies: - '@cspotcode/source-map-support': 0.8.1 - acorn: 8.8.1 - acorn-walk: 8.3.1 - capnp-ts: 0.7.0 - exit-hook: 2.2.1 - glob-to-regexp: 0.4.1 - stoppable: 1.1.0 - undici: 5.28.2 - workerd: 1.20240404.0 - ws: 8.11.0 - youch: 3.3.3 - zod: 3.22.4 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - - /ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - dev: false - - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /mustache@4.2.0: - resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} - hasBin: true - dev: true - - /nanoid@3.3.4: - resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true - - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true - - /next-tick@1.1.0: - resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} - dev: false - - /node-fetch@2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: false - - /node-forge@1.3.1: - resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} - engines: {node: '>= 6.13.0'} - dev: true - - /node-gyp-build@4.5.0: - resolution: {integrity: sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==} - hasBin: true - dev: false - - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - dev: true - - /nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - dependencies: - boolbase: 1.0.0 - dev: false - - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: true - - /optionator@0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} - engines: {node: '>= 0.8.0'} - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.3 - dev: true - - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - dev: true - - /parse5-htmlparser2-tree-adapter@7.0.0: - resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} - dependencies: - domhandler: 5.0.3 - parse5: 7.1.1 - dev: false - - /parse5@7.1.1: - resolution: {integrity: sha512-kwpuwzB+px5WUg9pyK0IcK/shltJN5/OVhQagxhCQNtT9Y9QRZqNY2e1cmbu/paRh5LMnz/oVTVLBpjFmMZhSg==} - dependencies: - entities: 4.4.0 - dev: false - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true - - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true - - /path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - dev: true - - /path-to-regexp@6.2.1: - resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} - dev: true - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true - - /prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} - engines: {node: '>=14'} - hasBin: true - dev: true - - /printable-characters@1.0.42: - resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==} - dev: true - - /punycode@2.1.1: - resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} - engines: {node: '>=6'} - dev: true - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - dependencies: - picomatch: 2.3.1 - dev: true - - /regexpp@3.2.0: - resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} - engines: {node: '>=8'} - dev: true - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true - - /resolve.exports@2.0.2: - resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} - engines: {node: '>=10'} - dev: true - - /resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - dev: true - - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - - /rollup-plugin-inject@3.0.2: - resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} - deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. - dependencies: - estree-walker: 0.6.1 - magic-string: 0.25.9 - rollup-pluginutils: 2.8.2 - dev: true - - /rollup-plugin-node-polyfills@0.2.1: - resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} - dependencies: - rollup-plugin-inject: 3.0.2 - dev: true - - /rollup-pluginutils@2.8.2: - resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} - dependencies: - estree-walker: 0.6.1 - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - - /selfsigned@2.1.1: - resolution: {integrity: sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==} - engines: {node: '>=10'} - dependencies: - node-forge: 1.3.1 - dev: true - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: true - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true - - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: true - - /sourcemap-codec@1.4.8: - resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} - deprecated: Please use @jridgewell/sourcemap-codec instead - dev: true - - /stacktracey@2.1.8: - resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} - dependencies: - as-table: 1.0.55 - get-source: 2.0.12 - dev: true - - /stoppable@1.1.0: - resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} - engines: {node: '>=4', npm: '>=6'} - dev: true - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: true - - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - dev: true - - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - - /tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - dev: false - - /ts-checked-fsm@1.1.0: - resolution: {integrity: sha512-kYbLNv4/orTm6TA0mdQmDy3k9teMmhyvzwtgYo3a0/TFjwDXi/g/3LkJlQd99fqqH5NxhWYLanNrwovb/r9PWA==} - dev: false - - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: true - - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - - /type@1.2.0: - resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} - dev: false - - /type@2.7.2: - resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==} - dev: false - - /typedarray-to-buffer@3.1.5: - resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} - dependencies: - is-typedarray: 1.0.0 - dev: false - - /ua-parser-js@1.0.32: - resolution: {integrity: sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==} - dev: false - - /undici@5.28.2: - resolution: {integrity: sha512-wh1pHJHnUeQV5Xa8/kyQhO7WFa8M34l026L5P/+2TYiakvGy5Rdc8jWZVyG7ieht/0WgJLEd3kcU5gKx+6GC8w==} - engines: {node: '>=14.0'} - dependencies: - '@fastify/busboy': 2.1.0 - dev: true - - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.1.1 - dev: true - - /utf-8-validate@5.0.10: - resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} - engines: {node: '>=6.14.2'} - requiresBuild: true - dependencies: - node-gyp-build: 4.5.0 - dev: false - - /webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - dev: false - - /websocket@1.0.34: - resolution: {integrity: sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==} - engines: {node: '>=4.0.0'} - dependencies: - bufferutil: 4.0.7 - debug: 2.6.9 - es5-ext: 0.10.62 - typedarray-to-buffer: 3.1.5 - utf-8-validate: 5.0.10 - yaeti: 0.0.6 - transitivePeerDependencies: - - supports-color - dev: false - - /whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - dev: false - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /word-wrap@1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} - engines: {node: '>=0.10.0'} - dev: true - - /workerd@1.20240404.0: - resolution: {integrity: sha512-U4tfnvBcPMsv7pmRGuF0J5UnoZi6tbc64tXNfyijI74r6w6Vlb2+a6eibdQL8g0g46+4vjuTKME9G5RvSvdc8g==} - engines: {node: '>=16'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@cloudflare/workerd-darwin-64': 1.20240404.0 - '@cloudflare/workerd-darwin-arm64': 1.20240404.0 - '@cloudflare/workerd-linux-64': 1.20240404.0 - '@cloudflare/workerd-linux-arm64': 1.20240404.0 - '@cloudflare/workerd-windows-64': 1.20240404.0 - dev: true - - /wrangler@3.48.0: - resolution: {integrity: sha512-Wv7JS6FyX1j9HkaM6WL3fmTzBMAYc4hPSyZCuxuH55hkJDX/7ts+YAgsaN1U8rKoDrV3FVSgBfI9TyqP9iuM8Q==} - engines: {node: '>=16.17.0'} - hasBin: true - peerDependencies: - '@cloudflare/workers-types': ^4.20240404.0 - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - dependencies: - '@cloudflare/kv-asset-handler': 0.3.1 - '@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19) - '@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19) - blake3-wasm: 2.1.5 - chokidar: 3.5.3 - esbuild: 0.17.19 - miniflare: 3.20240404.0 - nanoid: 3.3.4 - path-to-regexp: 6.2.1 - resolve: 1.22.8 - resolve.exports: 2.0.2 - selfsigned: 2.1.1 - source-map: 0.6.1 - xxhash-wasm: 1.0.2 - optionalDependencies: - fsevents: 2.3.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - dev: true - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true - - /ws@8.11.0: - resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: true - - /xxhash-wasm@1.0.2: - resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==} - dev: true - - /yaeti@0.0.6: - resolution: {integrity: sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug==} - engines: {node: '>=0.10.32'} - dev: false - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true - - /youch@3.3.3: - resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==} - dependencies: - cookie: 0.5.0 - mustache: 4.2.0 - stacktracey: 2.1.8 - dev: true - - /zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} - dev: true diff --git a/src/handler.js b/src/handler.js deleted file mode 100644 index 3208690..0000000 --- a/src/handler.js +++ /dev/null @@ -1,91 +0,0 @@ -import router from './router'; -import { json, error } from 'itty-router-extras'; -import { trace } from '@opentelemetry/api'; -import { BaselimeLogger } from '@baselime/edge-logger'; -import { createClient } from '@supabase/supabase-js'; - -export default { - /** - * @param {Request} req - */ - async fetch(req, env, ctx) { - const requestId = req.headers.get('cf-ray') || crypto.randomUUID(); - const span = trace.getActiveSpan(); - span?.setAttribute('cf-ray', requestId); - const logger = new BaselimeLogger({ - apiKey: env.BASELIME_API_KEY, - ctx, - dataset: 'cloudflare', - // isLocalDev: env.IS_LOCAL_MODE, - namespace: 'api', - requestId, - service: 'mue', - }); - ctx.$logger = logger; - ctx.$supabase = createClient(env.SUPABASE_URL, env.SUPABASE_TOKEN); - logger.info('Request', { - method: req.method, - origin: req.headers.get('origin'), - query: req.query, - requestId, - url: req.url, - }); - try { - const cache = caches.default; - const cacheKey = new Request(new URL(req.url).toString(), req); - - // attempt to serve from cache - if (req.method === 'GET') { - const res = await cache.match(cacheKey); - if (res) return res; - } - - // if not cached - - /** @type {Response} */ - let res = await router.handle(req, env, ctx); - - if (!(res instanceof Response)) { - res = json(res, { - // cdn 1d, client 1h, stale 1h - headers: { 'Cache-Control': 'public, s-max-age=86400, max-age=3600, stale-while-revalidate=3600' }, - }); - } - - res.headers.set('Access-Control-Allow-Origin', '*'); - - if (res.status > 399) { - // !res.ok - logger.warn('Non-ok response', { - request: { - method: req.method, - query: req.query, - url: req.url, - }, - response: { - body: await res.clone().text(), - status: res.status, - }, - }); - } - - - if (res.headers.has('Cache-Control') && res.headers.get('Cache-Control') !== 'no-store') { - ctx.waitUntil(cache.put(cacheKey, res.clone())); - } - - return res; - } catch (err) { - logger.error('Internal Serverless Error', { error: JSON.stringify(err) }); - const res = error(500, { - error: err?.message, - message: 'Internal Serverless Error', - stack: err?.stack, - }); - res.headers.set('Access-Control-Allow-Origin', '*'); - return res; - } finally { - ctx.waitUntil(logger.flush()); - } - }, -}; \ No newline at end of file diff --git a/src/index.js b/src/index.js deleted file mode 100644 index d2fa7aa..0000000 --- a/src/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import { instrument } from '@microlabs/otel-cf-workers'; -import handler from './handler'; - -const config = (env) => ({ - exporter: { - headers: { 'x-api-key': env.BASELIME_API_KEY }, - url: 'https://otel.baselime.io/v1', - }, - service: { name: 'mue' }, -}); - -export default instrument(handler, config); \ No newline at end of file diff --git a/src/pagination.js b/src/pagination.js deleted file mode 100644 index 14590ec..0000000 --- a/src/pagination.js +++ /dev/null @@ -1,13 +0,0 @@ -import { error } from 'itty-router-extras'; - -export default function paginate(data, query) { - if (query.offset) { - if (!query.limit) { - return error(400, 'Page limit is required for pagination'); - } - const limit = parseInt(query.limit); - const offset = parseInt(query.offset); - data = data.slice(offset, offset + limit); - } - return data; -} \ No newline at end of file diff --git a/src/router.js b/src/router.js deleted file mode 100644 index 3b7f496..0000000 --- a/src/router.js +++ /dev/null @@ -1,68 +0,0 @@ -import { Router } from 'itty-router'; -import { json, error } from 'itty-router-extras'; -import v2 from './v2'; -import sizes from './sizes'; -import news from '../news'; -import { getCollection, getCollections, getFeatured, getItem, getItems } from './v2/marketplace'; - -export default Router() - .get('/', () => 'Hello World! API docs: https://docs.muetab.com/api/introduction') - .get('/collection/:collection', getCollection) - .get('/collections', getCollections) - .get('/featured', getFeatured) - .get('/images/categories', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_image_categories'); - return data.map((row) => row.name); - }) - .get('/images/photographers', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_image_photographers'); - return data.map((row) => row.name); - }) - .get('/images/random', async (req, env, ctx) => { - const kv_id = 'image_categories'; - let categories = await env.cache.get(kv_id, { - cacheTtl: 3600, // cache at this location for an hour - type: 'json', - }); - if (!categories) { - const { data } = await ctx.$supabase.rpc('get_image_categories'); - // save for 1 day - ctx.waitUntil(env.cache.put(kv_id, JSON.stringify(data), { expirationTtl: 86400 })); - categories = data; - } - const category = categories[Math.floor(Math.random() * categories.length)].name; - const { data } = await ctx.$supabase.rpc('get_random_image', { _category: category }).single(); - const format = req.headers.get('accept')?.includes('avif') ? 'avif' : 'webp'; - const quality = sizes[req.query?.quality] ?? 'fhd'; - return json( - { - camera: data.camera, - category: data.category, - file: `https://cdn.muetab.com/img/${quality}/${data.id}.${format}?v=${data.version}`, - location: data.location_name, - photographer: data.photographer, - }, - { headers: { 'Cache-Control': 'no-store' } }, - ); - }) - .get('/item/:category/:item', getItem) - .get('/items/:category', getItems) - .get('/news', () => ({ news })) - .get('/quotes/languages', () => ['English', 'French']) - .get('/quotes/random', async (req, env, ctx) => { - const language = req.query.language?.replace('French', 'Français') || 'English'; - const { data } = await ctx.$supabase - .rpc('get_random_old_quote', { _language: language }) - .single(); - return data; - }) - .get('/stats', async (req, env) => { - const res = await env.WEBSTORES.fetch(req); - return new Response(res.body, res); - }) - .get('/versions', async (req, env) => { - const res = await env.WEBSTORES.fetch(req); - return new Response(res.body, res); - }) - .all('/v2/*', v2.handle) - .all('*', () => error(404, 'Not Found')); diff --git a/src/sizes.js b/src/sizes.js deleted file mode 100644 index ad81387..0000000 --- a/src/sizes.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - original: 'original', - high: 'qhd', // eslint-disable-line sort-keys - normal: 'fhd', - datasaver: 'hd', // eslint-disable-line sort-keys -}; diff --git a/src/v2/index.js b/src/v2/index.js deleted file mode 100644 index 43ac603..0000000 --- a/src/v2/index.js +++ /dev/null @@ -1,186 +0,0 @@ -import { Router } from 'itty-router'; -import { json, error } from 'itty-router-extras'; -import sizes from '../sizes'; -import news from '../../news'; -import { getUnsplashImage } from './unsplash'; -import { getPexelsImage } from './pexels'; -import { withWeatherLanguage } from './weather'; -import { - getCollection, - getCollections, - getCurator, - getCurators, - getFeatured, - getItem, - getItems, -} from './marketplace'; - -export default Router({ base: '/v2' }) - .get('/marketplace/collection/:collection', getCollection) - .get('/marketplace/collections', getCollections) - .get('/marketplace/curator/:curator', getCurator) - .get('/marketplace/curators', getCurators) - .get('/marketplace/featured', getFeatured) - .get('/marketplace/item/:category/:item', getItem) - .get('/marketplace/items/:category', getItems) - .get('/gps', withWeatherLanguage, async (req, env, ctx) => { - const { latitude, longitude } = req.query; - if (!latitude || !longitude) { - return error(400, '`latitude` and `longitude` params are required'); - } - const url = `https://api.openweathermap.org/geo/1.0/reverse?lat=${latitude}&lon=${longitude}&limit=1&appid=${env.OPENWEATHER_TOKEN}&lang=${ctx.$language}`; - const data = await (await fetch(url)).json(); - return json(data, { - headers: { - // 1w, stale 1d - 'Cache-Control': 'public, max-age=604800, stale-while-revalidate=86400, immutable', - }, - }); - }) - .get('/images/categories', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_image_categories'); - return data; - }) - .get('/images/pexels', async (req, ...rest) => { - const data = await getPexelsImage(req.query.quality ?? 'normal', ...rest); - return json(data, { headers: { 'Cache-Control': 'no-store' } }); - }) - .get('/images/photographers', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_image_photographers'); - return data; - }) - .get('/images/random', async (req, env, ctx) => { - const kv_id = 'v2_image_categories'; - let allowed = await env.cache.get(kv_id, { - cacheTtl: 3600, // cache at this location for an hour - type: 'json', - }); - if (!allowed) { - const { data } = await ctx.$supabase.rpc('get_image_categories'); - // save for 1 day - ctx.waitUntil(env.cache.put(kv_id, JSON.stringify(data), { expirationTtl: 86400 })); - allowed = data; - } - allowed = allowed.map((row) => row.name); - let categories = - req.query.categories?.split(',')?.filter((category) => allowed.includes(category)) ?? []; - if (categories.length === 0) categories = allowed; - const category = categories[Math.floor(Math.random() * categories.length)]; - const { data } = await ctx.$supabase - .rpc('get_random_image', { - _category: category, - _exclude: req.query.exclude, - }) - .single(); - const format = req.headers.get('accept')?.includes('avif') ? 'avif' : 'webp'; - const quality = sizes[req.query?.quality] ?? 'fhd'; - const coordinates = data.location_data?.split(','); - return json( - { - blur_hash: data.blur_hash, - camera: data.camera, - category: data.category, - colour: data.colour, - file: `https://cdn.muetab.com/img/${quality}/${data.id}.${format}?v=${data.version}`, - id: data.id, - location: { - latitude: coordinates?.[0] ?? null, - longitude: coordinates?.[1] ?? null, - name: data.location_name, - }, - photographer: data.photographer, - pun: data.pun, - }, - { headers: { 'Cache-Control': 'no-store' } }, - ); - }) - .get('/images/unsplash', async (req, ...rest) => { - const named_collections = { - animals: 'nJDnd_8TN_g', - architecture: 'e9-QAhrwZ5Q', - landscapes: 'SxeKQtPuR0U', - plants: 'y15m5OvaD98', - }; - const { categories, collections, orientation, topics, username } = req.query; - const unsplash_query = new URLSearchParams({ orientation: orientation ?? 'landscape' }); - if (categories && categories.length > 0) { - unsplash_query.set('collections', categories - .split(',') - .map((category) => named_collections[category]) - .join(',')); - } - // collections overwrites categories - if (collections !== undefined) unsplash_query.set('collections', collections); - if (topics !== undefined) unsplash_query.set('topics', topics); - if (username !== undefined) unsplash_query.set('username', username); - if (unsplash_query.get('collections') === null) unsplash_query.set('collections', Object.values(named_collections).join(',')); - const data = await getUnsplashImage(unsplash_query, req.query.quality ?? 'normal', ...rest); - return json(data, { headers: { 'Cache-Control': 'no-store' } }); - }) - .get('/images/unsplash/topics', async (req, env) => { - const data = await ( - await fetch(`https://api.unsplash.com/topics?client_id=${env.UNSPLASH_TOKEN}`, { - cf: { cacheTtl: 86400 }, - }) - ).json(); - return json(data, { - // cdn 1w, client 1d, stale 1d - headers: { 'Cache-Control': 'public, s-max-age=604800, max-age=86400, stale-while-revalidate=86400' }, - }); - }) - .get('/quotes/languages', async (req, env, ctx) => { - const { data } = await ctx.$supabase.rpc('get_quote_languages'); - return data; - }) - .get('/map', async (req, env) => { - const { latitude, longitude } = req.query; - if (!latitude) return error(400, 'latitude is required'); - if (!longitude) return error(400, 'longitude is required'); - const url = `https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/pin-s+555555(${longitude},${latitude})/${longitude},${latitude},9},0/450x200?access_token=${env.MAPBOX_TOKEN}`; - const res = await fetch(url, { cf: { cacheTtl: 31536000 } }); // 1 year - return new Response(res.body, res); - }) - .get('/news', () => news) - .get('/quotes/random', async (req, env, ctx) => { - const kv_id = 'v2_quote_languages'; - let allowed = await env.cache.get(kv_id, { - cacheTtl: 3600, // cache at this location for an hour - type: 'json', - }); - if (!allowed) { - const { data } = await ctx.$supabase.rpc('get_quote_languages'); - // save for 1 day - ctx.waitUntil(env.cache.put(kv_id, JSON.stringify(data), { expirationTtl: 86400 })); - allowed = data; - } - allowed = allowed.map((row) => row.name); - const language = req.query.language || 'en'; - if (!allowed.includes(language)) return error(400, 'Unsupported language'); - const { data } = await ctx.$supabase.rpc('get_random_quote', { _language: language }).single(); - return json(data, { headers: { 'Cache-Control': 'no-store' } }); - }) - .get('/search/autocomplete', async (req) => { - const search = new URLSearchParams(req.query).toString(); - const url = 'https://ac.ecosia.org/?' + search; - const headers = new Headers(req.headers); - headers.append('x-forwarded-for', req.headers.get('cf-connecting-ip')); - const res = await fetch(new Request(url, { headers })); - return json(await res.json()); - }) - .get('/stats', async (req, env) => { - const res = await env.WEBSTORES.fetch(req); - return new Response(res.body, res); - }) - .get('/versions', async (req, env) => { - const res = await env.WEBSTORES.fetch(req); - return new Response(res.body, res); - }) - .get('/weather', withWeatherLanguage, async (req, env, ctx) => { - const { city } = req.query; - if (!city) return error(400, '`city` param is required'); - const url = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${env.OPENWEATHER_TOKEN}&lang=${ctx.$language}`; - const data = await (await fetch(url)).json(); - if (data.cod === '404') return error(404, 'No data. Try another city?'); - // 10m (too short for cf), stale 5m - return json(data, { headers: { 'Cache-Control': 'private, max-age=600, stale-while-revalidate=300' } }); - }); diff --git a/src/v2/marketplace.js b/src/v2/marketplace.js deleted file mode 100644 index 3fc0471..0000000 --- a/src/v2/marketplace.js +++ /dev/null @@ -1,170 +0,0 @@ -import { error, json } from 'itty-router-extras'; -import paginate from '../pagination.js'; - -/** - * @param {Request} req - */ -function getVersion(req) { - const url = new URL(req.url); - const path = url.pathname; - let version = path.split('/')[1]; - if (!version.startsWith('v')) version = 1; - else version = parseInt(version.slice(1)); - return version; -} - -async function getManifest() { - const manifest = await ( - await fetch('https://marketplace-data.muetab.com/manifest.json?v=2', { cf: { cacheTtl: 3600 } }) - ).json(); - return manifest; -} - -/** - * @param {Request} req - */ -export async function getCollection(req) { - const manifest = await getManifest(); - const collection = manifest.collections[req.params.collection]; - if (collection === undefined) { - return error(404, 'Not Found'); - } - const unresolved_items = collection.items; - collection.items = unresolved_items.map((item) => { - const [type, name] = item.split('/'); - return { - ...manifest[type][name], - type, - }; - }); - - const version = getVersion(req); - if (version === 1) { - collection.name = collection.display_name; - delete collection.display_name; - } - - return json({ data: collection }); -} - -export async function getCollections(req) { - const manifest = await getManifest(); - const collections = Object.values(manifest.collections).map((collection) => { - delete collection.items; - return collection; - }); - return json({ data: paginate(collections, req.query) }); -} - -/** - * @param {Request} req - */ -export async function getCurator(req) { - const manifest = await getManifest(); - const curator = manifest.curators[decodeURIComponent(req.params.curator)]; - if (curator === undefined) { - return error(404, 'Not Found'); - } - const items = curator.map((item) => { - const [type, name] = item.split('/'); - return { - ...manifest[type][name], - type, - }; - }); - return json({ data: { items } }); -} - -export async function getCurators(req) { - const manifest = await getManifest(); - const curators = Object.keys(manifest.curators); - return json({ data: paginate(curators, req.query) }); -} - -export async function getFeatured() { - return json({ - data: await ( - await fetch('https://marketplace-data.muetab.com/featured.json', { cf: { cacheTtl: 3600 } }) - ).json(), - }); -} - -/** - * @param {Request} req - */ -export async function getItem(req) { - const manifest = await getManifest(); - if (!manifest[req.params.category]) { - return error(404, 'Category Not Found'); - } - if (manifest[req.params.category][req.params.item] === undefined) { - return error(404, 'Item Not Found'); - } - let item = await ( - await fetch( - `https://marketplace-data.muetab.com/${req.params.category}/${req.params.item}.json`, - { cf: { cacheTtl: 3600 } }, - ) - ).json(); - - item.in_collections = manifest[req.params.category][req.params.item].in_collections.map( - (name) => { - const collection = manifest.collections[name]; - delete collection.items; - return collection; - }, - ); - - const version = getVersion(req); - if (version === 2) { - item = { - display_name: item.name, - ...item, - name: req.params.item, - }; - } - - return json({ - data: item, - updated: item.updated_at, - }); -} - -/** - * @param {Request} req - */ -export async function getItems(req) { - let data; - const manifest = await getManifest(); - if (req.params.category === 'all') { - data = [ - ...Object.values(manifest.preset_settings).map((item) => { - item.type = 'preset_settings'; - return item; - }), - ...Object.values(manifest.photo_packs).map((item) => { - item.type = 'photo_packs'; - return item; - }), - ...Object.values(manifest.quote_packs).map((item) => { - item.type = 'quote_packs'; - return item; - }), - ]; - } else { - const category = manifest[req.params.category]; - if (!category) { - return error(404, 'Not Found'); - } - data = Object.values(category); - const version = getVersion(req); - if (version === 1) { - data = data.map((item) => { - item.type = req.params.category; - return item; - }); - } - } - data = paginate(data, req.query); - return json({ data }); -} diff --git a/src/v2/pexels.js b/src/v2/pexels.js deleted file mode 100644 index 60c5383..0000000 --- a/src/v2/pexels.js +++ /dev/null @@ -1,24 +0,0 @@ -const sizes = { - original: '', - high: '?w=3840', // eslint-disable-line sort-keys - normal: '?w=1920', - datasaver: '?w=1280', // eslint-disable-line sort-keys -}; - -export const getPexelsImage = async (quality, ...rest) => { - const [env] = rest; - const size = sizes[quality]; - const url = `https://api.pexels.com/v1/collections/${env.PEXELS_COLLECTION}?per_page=80&page=${ - Math.floor(Math.random() * 2) + 1 - }`; - let data = await (await fetch(url, { headers: { Authorization: env.PEXELS_TOKEN } })).json(); - data = data.media[Math.floor(Math.random() * data.media.length) + 1]; - return { - colour: data.average_color, - file: data.src.original + size, - id: data.id, - photo_page: data.url, - photographer: data.photographer, - photographer_page: data.photographer_url, - }; -}; diff --git a/src/v2/sponsors.js b/src/v2/sponsors.js deleted file mode 100644 index 7cb8780..0000000 --- a/src/v2/sponsors.js +++ /dev/null @@ -1,16 +0,0 @@ -import cheerio from 'cheerio'; - -export const getSponsors = async () => { - const data = await ( - await fetch(`https://github.com/sponsors/${env.SPONSORS_NAME}/sponsors_partial?page=1`) - ).text(); - const $ = cheerio.load(data); - const sponsors = []; - $('.d-inline-block').each((i, el) => { - sponsors.push({ - img: $(el).attr('href').replace('/', ''), - name: $(el).find('img').attr('alt'), - }); - }); - return { sponsors }; -}; diff --git a/src/v2/unsplash.js b/src/v2/unsplash.js deleted file mode 100644 index 737f20c..0000000 --- a/src/v2/unsplash.js +++ /dev/null @@ -1,37 +0,0 @@ -const sizes = { - original: '', - high: '&w=3840', // eslint-disable-line sort-keys - normal: '&w=1920', - datasaver: '&w=1280', // eslint-disable-line sort-keys -}; - -export const getUnsplashImage = async (query, quality, ...rest) => { - const [env, ctx] = rest; - query.set('client_id', env.UNSPLASH_TOKEN); - const ref = `?utm_source=${env.UNSPLASH_REFERRAL}&utm_medium=referral`; - const size = sizes[quality]; - const data = await ( - await fetch(`https://api.unsplash.com/photos/random?${query.toString()}`) - ).json(); - ctx.waitUntil(fetch(`${data.links.download_location}&client_id=${env.UNSPLASH_TOKEN}`)); // api requirement - return { - blur_hash: data.blur_hash, - camera: data.exif.model ?? null, - category: null, - colour: data.color, - description: data.description ?? null, - downloads: data.downloads, - file: data.urls.raw + '&q=80&auto=format' + size, - id: data.id, - likes: data.likes, - location: { - latitude: data.location?.position?.latitude ?? null, - longitude: data.location?.position?.longitude ?? null, - name: data.location?.name ?? null, - }, - photo_page: data.links.html + ref, - photographer: data.user.name, - photographer_page: data.user.links.html + ref, // api requirement - views: data.views, - }; -}; diff --git a/src/v2/weather.js b/src/v2/weather.js deleted file mode 100644 index e7d2286..0000000 --- a/src/v2/weather.js +++ /dev/null @@ -1,24 +0,0 @@ -import { error } from 'itty-router-extras'; - -export const withWeatherLanguage = (req, env, ctx) => { - const allowed = ['en', 'de', 'es', 'fr', 'nl', 'no', 'ru', 'zh_cn', 'id', 'tr', 'pt', 'bn']; - const map = new Map([ - ['de_DE', 'de'], - ['en_GB', 'en'], - ['en_US', 'en'], - ['zh_CN', 'zh_cn'], - ['pt_BR', 'pt'], - ['es_419', 'es'], - ['tr_TR', 'tr'], - ['id_ID', 'id'], - ]); - let { language } = req.query; - if (!language) { - language = 'en'; - } else if (map.has(language)) { - language = map.get(language); - } else if (!allowed.includes(language)) { - return error(400, 'Unsupported language'); - } - ctx.$language = language; -}; diff --git a/supabase.sql b/supabase.sql deleted file mode 100644 index 5595b36..0000000 --- a/supabase.sql +++ /dev/null @@ -1,108 +0,0 @@ -create or replace index on "public"."images" using btree (category); - -create or replace index on "public"."quotes" using btree (language); - -create or replace function get_image_categories() -returns table ( - name text, - count bigint -) -language plpgsql -as $$ -begin - return query - select - category as name, - count(category) - from images - group by category - order by count(category) desc; -end;$$; - - -create or replace function get_image_photographers() -returns table ( - name text, - count bigint -) -language plpgsql -as $$ -begin - return query - select - photographer as name, - count(photographer) - from images - group by photographer - order by count(photographer) desc; -end;$$; - - -create or replace function get_quote_languages() -returns table ( - name text, - count bigint -) -language plpgsql -as $$ -begin - return query - select - language as name, - count(language) - from quotes - group by language - order by count(language) desc; -end;$$; - - -create or replace function get_random_image( - _category text default 'landscapes', - _exclude text default '' -) -returns setof images -language plpgsql -as $$ -begin - return query - select * - from images - where - category = _category - and - cast(pun as text) not in (select * from unnest(string_to_array(_exclude, ','))) - order by random() - limit 1; -end;$$; - - -create or replace function get_random_old_quote( - _language text default 'English' -) -returns setof old_quotes -language plpgsql -as $$ -begin - return query - select * - from old_quotes - where language = _language - order by random() - limit 1; -end;$$; - - -create or replace function get_random_quote( - _language text default 'en' -) -returns setof quotes -language plpgsql -as $$ -begin - return query - select * - from quotes - where language = _language - order by random() - limit 1; -end;$$; diff --git a/wrangler.toml b/wrangler.toml deleted file mode 100644 index 6d770d1..0000000 --- a/wrangler.toml +++ /dev/null @@ -1,36 +0,0 @@ -account_id = "950e96e5beb222c4eeb9962512a6d6fc" -name = "api" -main = "src/index.js" -compatibility_date = "2024-04-05" -compatibility_flags = [ "nodejs_compat" ] -upload_source_maps = true -workers_dev = true -routes = [ - { pattern = "api.muetab.com", custom_domain = true, zone_name = "muetab.com" }, - { pattern = "marketplace.muetab.com", custom_domain = true, zone_name = "muetab.com" } -] -kv_namespaces = [ - { binding = "cache", id = "08c0008096b7486a9532c5be43e48a7f" } -] - -[[services]] - binding = "WEBSTORES" - service = "webstores-api" - -# [[d1_databases]] -# binding = "DB" -# database_name = "mue-content" -# database_id = "5b6376e6-1d2a-4f3b-b463-0d6a1b387be1" - -[vars] - UNSPLASH_REFERRAL = "mue" - PEXELS_COLLECTION = "iwf78dt" - SPONSORS_NAME = "mue" - -# SECRETS: -# SUPABASE_URL -# SUPABASE_TOKEN -# UNSPLASH_TOKEN -# PEXELS_TOKEN -# MAPBOX_TOKEN -# OPENWEATHER_TOKEN From fa96f07a710641a2ef43b95ee34695ecd41c1743 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 28 Aug 2024 00:21:32 +0100 Subject: [PATCH 02/43] feat: v3 API initial commit --- .gitignore | 22 +++- go.mod | 11 ++ go.sum | 8 ++ internal/database/sqlite.go | 101 +++++++++++++++++ internal/handlers/quote_handler.go | 164 +++++++++++++++++++++++++++ internal/handlers/root_handler.go | 38 +++++++ internal/models/quote.go | 173 +++++++++++++++++++++++++++++ main.go | 65 +++++++++++ 8 files changed, 580 insertions(+), 2 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/database/sqlite.go create mode 100644 internal/handlers/quote_handler.go create mode 100644 internal/handlers/root_handler.go create mode 100644 internal/models/quote.go create mode 100644 main.go diff --git a/.gitignore b/.gitignore index 5f7abc4..a7aa63c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,26 @@ .vscode/ .history/ .wrangler/ -node_modules/ # Files .env -.dev.vars \ No newline at end of file +.dev.vars + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +# Test binary, built with `go test -c` +*.test +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +*.db + +# Dependency directories (remove the comment below to include it) +# vendor/ +# Go workspace file +go.work +go.work.sum + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d964afe --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module quote-api + +go 1.23.0 + +require ( + github.com/go-chi/chi/v5 v5.1.0 + github.com/google/uuid v1.6.0 + github.com/mattn/go-sqlite3 v1.14.22 +) + +require github.com/joho/godotenv v1.5.1 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0b19ca1 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go new file mode 100644 index 0000000..38165e0 --- /dev/null +++ b/internal/database/sqlite.go @@ -0,0 +1,101 @@ +package database + +import ( + "database/sql" + "log" + + "github.com/google/uuid" + _ "github.com/mattn/go-sqlite3" +) + +func ConnectDB(dbPath string) (*sql.DB, error) { + db, err := sql.Open("sqlite3", dbPath) + if err != nil { + return nil, err + } + + // Ping the database to ensure the connection is established + if err := db.Ping(); err != nil { + return nil, err + } + + return db, nil +} + +func InitDB(db *sql.DB) { + createTableQuery := ` + CREATE TABLE IF NOT EXISTS quotes ( + id TEXT PRIMARY KEY, + quote TEXT NOT NULL, + author TEXT NOT NULL, + author_occupation TEXT NOT NULL, + language TEXT NOT NULL DEFAULT 'en' + );` + + _, err := db.Exec(createTableQuery) + if err != nil { + log.Fatalf("Failed to create table: %v", err) + } + + // Optional: Insert sample data if the table is empty + row := db.QueryRow("SELECT COUNT(*) FROM quotes") + var count int + if err := row.Scan(&count); err != nil { + log.Fatalf("Failed to count rows: %v", err) + } + + if count == 0 { + insertSampleData(db) + } +} + +func insertSampleData(db *sql.DB) { + insertQuery := ` + INSERT INTO quotes (id, quote, author, author_occupation, language) VALUES + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?), + (?, ?, ?, ?, ?);` + + _, err := db.Exec(insertQuery, + uuid.New().String(), "The only limit to our realization of tomorrow is our doubts of today.", "Franklin D. Roosevelt", "32nd U.S. President", "en", + uuid.New().String(), "In the end, we will remember not the words of our enemies, but the silence of our friends.", "Martin Luther King Jr.", "Civil Rights Leader", "en", + uuid.New().String(), "The greatest glory in living lies not in never falling, but in rising every time we fall.", "Nelson Mandela", "Former President of South Africa", "en", + uuid.New().String(), "La única limitación para nuestra realización del mañana son nuestras dudas de hoy.", "Franklin D. Roosevelt", "32º Presidente de los EE.UU.", "es", + uuid.New().String(), "Al final, no recordaremos las palabras de nuestros enemigos, sino el silencio de nuestros amigos.", "Martin Luther King Jr.", "Líder de los Derechos Civiles", "es", + uuid.New().String(), "La mayor gloria en la vida no radica en no caer nunca, sino en levantarnos cada vez que caemos.", "Nelson Mandela", "Ex Presidente de Sudáfrica", "es", + uuid.New().String(), "La seule limite à notre réalisation de demain est nos doutes d'aujourd'hui.", "Franklin D. Roosevelt", "32e Président des États-Unis", "fr", + uuid.New().String(), "À la fin, nous nous souviendrons non pas des mots de nos ennemis, mais du silence de nos amis.", "Martin Luther King Jr.", "Leader des droits civiques", "fr", + uuid.New().String(), "La plus grande gloire de vivre ne réside pas dans le fait de ne jamais tomber, mais de se relever à chaque fois que nous tombons.", "Nelson Mandela", "Ancien Président de l'Afrique du Sud", "fr", + uuid.New().String(), "Die einzige Grenze zu unserer Verwirklichung von morgen sind unsere Zweifel von heute.", "Franklin D. Roosevelt", "32. Präsident der USA", "de", + uuid.New().String(), "Am Ende werden wir uns nicht an die Worte unserer Feinde erinnern, sondern an das Schweigen unserer Freunde.", "Martin Luther King Jr.", "Bürgerrechtsführer", "de", + uuid.New().String(), "Der größte Ruhm im Leben liegt nicht darin, niemals zu fallen, sondern jedes Mal aufzustehen, wenn wir fallen.", "Nelson Mandela", "Ehemaliger Präsident von Südafrika", "de", + uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", + uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", + uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", + uuid.New().String(), "La única limitación para nuestra realización del mañana son nuestras dudas de hoy.", "Franklin D. Roosevelt", "32º Presidente de los EE.UU.", "es", + uuid.New().String(), "Al final, no recordaremos las palabras de nuestros enemigos, sino el silencio de nuestros amigos.", "Martin Luther King Jr.", "Líder de los Derechos Civiles", "es", + uuid.New().String(), "La mayor gloria en la vida no radica en no caer nunca, sino en levantarnos cada vez que caemos.", "Nelson Mandela", "Ex Presidente de Sudáfrica", "es", + uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", + uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", + uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", + uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", + uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", + uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", + uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", + uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", + uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", + uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", + uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", + uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", + ) + if err != nil { + log.Fatalf("Failed to insert sample data: %v", err) + } +} diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go new file mode 100644 index 0000000..94ccb37 --- /dev/null +++ b/internal/handlers/quote_handler.go @@ -0,0 +1,164 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "log" + "net/http" + "strings" + + "quote-api/internal/models" + + "github.com/go-chi/chi/v5" +) + +type QuoteHandler struct { + DB *sql.DB +} + +func (h *QuoteHandler) GetQuoteLanguages(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + languages, err := models.GetQuoteLanguages(ctx, h.DB) + if err != nil { + http.Error(w, "Failed to get quote languages", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(languages) +} + +func (h *QuoteHandler) GetAllQuotes(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + language := r.URL.Query().Get("language") + quotes, err := models.GetAllQuotes(ctx, h.DB, language) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(quotes) +} + +func (h *QuoteHandler) GetQuoteByID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := chi.URLParam(r, "id") + language := r.URL.Query().Get("language") + quote, err := models.GetQuoteByID(ctx, h.DB, id, language) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(quote) +} + +func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + language := r.URL.Query().Get("language") + + // If no language query parameter is provided, use the Accept-Language header + if language == "" { + language = parseAcceptLanguage(r.Header.Get("Accept-Language")) + if language == "" { + language = "en" // Default to English if no Accept-Language header is present + } + } + + // Retrieve the list of quote IDs from the client's cookies + cookie, err := r.Cookie("seen_quotes") + var seenQuotes []string + if err == nil { + seenQuotes = strings.Split(cookie.Value, ",") + } else { + // If the cookie does not exist, start with an empty list + seenQuotes = []string{} + log.Println("No seen_quotes cookie found, starting with an empty list") + } + + // Function to fetch a random quote with fallback to English + fetchQuote := func(language string) (*models.Quote, error) { + quote, err := models.GetRandomQuoteExcluding(ctx, h.DB, language, seenQuotes) + if err != nil && strings.Contains(err.Error(), "no quotes found") { + // If no quotes are found, reset the seenQuotes list and try again + log.Println("No quotes found, resetting seenQuotes list") + seenQuotes = []string{} + quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, language, seenQuotes) + } + return quote, err + } + + // Try to fetch a quote in the requested language + quote, err := fetchQuote(language) + if err != nil { + // If no quotes found, default to English + quote, err = fetchQuote("en") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + // Add the new quote ID to the seenQuotes list if it’s not already there + if !contains(seenQuotes, quote.ID) { + seenQuotes = append(seenQuotes, quote.ID) + } + + // Debugging: Log the updated list of seen quotes + log.Printf("Selected quote ID: %s", quote.ID) + log.Printf("Updated seen quotes list: %s", strings.Join(seenQuotes, ",")) + + // Update the cookie with the new list of seen quote IDs + seenQuotesStr := strings.Join(seenQuotes, ",") + newCookie := &http.Cookie{ + Name: "seen_quotes", + Value: seenQuotesStr, + Path: "/", + // Consider setting MaxAge or Expires if you want the cookie to persist across sessions + } + + // Debugging: Log the cookie value before setting it + log.Printf("Setting seen_quotes cookie with value: %s", seenQuotesStr) + + http.SetCookie(w, newCookie) + + // Return the quote as JSON + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(quote); err != nil { + log.Printf("Error encoding response: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +// Helper function to check if a slice contains a string +func contains(slice []string, item string) bool { + for _, s := range slice { + if s == item { + return true + } + } + return false +} + +// parseAcceptLanguage simplifies the Accept-Language header to a basic language code +func parseAcceptLanguage(header string) string { + if header == "" { + return "" + } + + // Split the header by commas to handle multiple languages + parts := strings.Split(header, ",") + if len(parts) > 0 { + // The first part should be the most preferred language + lang := strings.TrimSpace(parts[0]) + + // Split by ';' to remove any quality value (q=...) + if semicolonIdx := strings.Index(lang, ";"); semicolonIdx != -1 { + lang = lang[:semicolonIdx] + } + + // Return only the first 2 characters (e.g., "en-US" -> "en") + if len(lang) >= 2 { + return lang[:2] + } + } + return "" +} diff --git a/internal/handlers/root_handler.go b/internal/handlers/root_handler.go new file mode 100644 index 0000000..2430d57 --- /dev/null +++ b/internal/handlers/root_handler.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "encoding/json" + "net/http" +) + +type Endpoint struct { + Method string `json:"method"` + Path string `json:"path"` + Description string `json:"description"` +} + +type ApiInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Endpoints []Endpoint `json:"endpoints"` + DocumentationURL string `json:"documentation_url"` +} + +// rootHandler handles requests to the root endpoint. +func RootHandler(w http.ResponseWriter, r *http.Request) { + apiInfo := ApiInfo{ + Name: "Quote API", + Version: "1.0.0", + Description: "A simple API for fetching inspirational quotes.", + Endpoints: []Endpoint{ + {Method: "GET", Path: "/quotes", Description: "Retrieve a list of all quotes"}, + {Method: "GET", Path: "/quotes/{id}", Description: "Retrieve a quote by its ID"}, + {Method: "GET", Path: "/quotes/random", Description: "Retrieve a random quote"}, + }, + DocumentationURL: "https://docs.muetab.com/", + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(apiInfo) +} diff --git a/internal/models/quote.go b/internal/models/quote.go new file mode 100644 index 0000000..037b06e --- /dev/null +++ b/internal/models/quote.go @@ -0,0 +1,173 @@ +package models + +import ( + "context" + "database/sql" + "errors" + "fmt" + "log" + "math/rand" + "strings" + "time" +) + +// Quote represents a quote with its details. +type Quote struct { + ID string + Quote string + Author string + Occupation string +} + +// LanguageCount represents a language and its quote count. +type LanguageCount struct { + Language string + Count int +} + +var rng *rand.Rand + +// Initialize the random generator once at package initialization. +func init() { + rng = rand.New(rand.NewSource(time.Now().UnixNano())) +} + +// GetQuoteLanguages returns the available languages and the count of quotes for each language. +func GetQuoteLanguages(ctx context.Context, db *sql.DB) ([]LanguageCount, error) { + query := ` + SELECT language, COUNT(*) as count + FROM quotes + GROUP BY language + ` + + rows, err := db.QueryContext(ctx, query) + if err != nil { + log.Printf("Error querying quote languages: %v", err) + return nil, err + } + defer rows.Close() + + var languages []LanguageCount + for rows.Next() { + var lc LanguageCount + if err := rows.Scan(&lc.Language, &lc.Count); err != nil { + log.Printf("Error scanning language count: %v", err) + return nil, err + } + languages = append(languages, lc) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return languages, nil +} + +// GetAllQuotes fetches all quotes in the specified language. +func GetAllQuotes(ctx context.Context, db *sql.DB, language string) ([]Quote, error) { + if language == "" { + language = "en" + } + + query := "SELECT id, quote, author, author_occupation FROM quotes WHERE language = ?" + rows, err := db.QueryContext(ctx, query, language) + if err != nil { + log.Printf("Error querying all quotes: %v", err) + return nil, err + } + defer rows.Close() + + var quotes []Quote + for rows.Next() { + var q Quote + if err := rows.Scan(&q.ID, &q.Quote, &q.Author, &q.Occupation); err != nil { + log.Printf("Error scanning quote: %v", err) + return nil, err + } + quotes = append(quotes, q) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return quotes, nil +} + +// GetQuoteByID fetches a quote by its ID in the specified language. +func GetQuoteByID(ctx context.Context, db *sql.DB, id string, language string) (Quote, error) { + if language == "" { + language = "en" + } + + var quote Quote + query := "SELECT id, quote, author, author_occupation FROM quotes WHERE id = ? AND language = ?" + err := db.QueryRowContext(ctx, query, id, language).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.Printf("No quote found with id %s and language %s", id, language) + return quote, errors.New("no quote found") + } + log.Printf("Error querying quote by ID: %v", err) + return quote, err + } + + return quote, nil +} + +// GetRandomQuote fetches a random quote in the specified language. +func GetRandomQuote(ctx context.Context, db *sql.DB, language string) (Quote, error) { + if language == "" { + language = "en" + } + + var quote Quote + query := "SELECT id, quote, author, author_occupation FROM quotes WHERE language = ? ORDER BY RANDOM() LIMIT 1" + err := db.QueryRowContext(ctx, query, language).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.Printf("No quotes found for language %s", language) + return quote, errors.New("no quotes found") + } + log.Printf("Error querying random quote: %v", err) + return quote, err + } + + return quote, nil +} + +func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, language string, exclude []string) (*Quote, error) { + excludeClause := "" + if len(exclude) > 0 { + placeholders := strings.Repeat("?,", len(exclude)-1) + "?" + excludeClause = fmt.Sprintf("AND id NOT IN (%s)", placeholders) + } + + query := fmt.Sprintf(` + SELECT id, quote, author, author_occupation + FROM quotes + WHERE language = ? %s + ORDER BY RANDOM() + LIMIT 1 + `, excludeClause) + + args := make([]interface{}, len(exclude)+1) + args[0] = language + for i, id := range exclude { + args[i+1] = id + } + + row := db.QueryRowContext(ctx, query, args...) + var quote Quote + if err := row.Scan("e.ID, "e.Quote, "e.Author, "e.Occupation); err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("no quotes found for language %s excluding provided IDs", language) + } + return nil, err + } + + return "e, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..acf23ef --- /dev/null +++ b/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "log" + "net/http" + "net/http/pprof" // Import pprof + "os" + + "quote-api/internal/database" + "quote-api/internal/handlers" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/joho/godotenv" +) + +func main() { + // Load environment variables from .env file + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + dbPath := os.Getenv("DB_PATH") + if dbPath == "" { + log.Fatal("DB_PATH environment variable is not set") + } + + db, err := database.ConnectDB(dbPath) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + defer db.Close() + + database.InitDB(db) + + quoteHandler := &handlers.QuoteHandler{DB: db} + + r := chi.NewRouter() + + // Middleware + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + + // Routes + r.Get("/", handlers.RootHandler) + + r.Get("/quotes", quoteHandler.GetAllQuotes) + r.Get("/quotes/{id}", quoteHandler.GetQuoteByID) + r.Get("/quotes/random", quoteHandler.GetRandomQuote) + r.Get("/quotes/languages", quoteHandler.GetQuoteLanguages) + + // pprof routes + r.HandleFunc("/debug/pprof/*", pprof.Index) + r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + r.HandleFunc("/debug/pprof/profile", pprof.Profile) + r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + r.HandleFunc("/debug/pprof/trace", pprof.Trace) + + log.Println("Starting server on :8080") + err = http.ListenAndServe(":8080", r) + if err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} From c13e07c0010bb8f877cabbc1607eb96b6ebe5ce0 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 28 Aug 2024 17:45:35 +0100 Subject: [PATCH 03/43] fix: make compatible with old quote DB table name --- internal/database/sqlite.go | 2 +- internal/models/quote.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go index 38165e0..1e92138 100644 --- a/internal/database/sqlite.go +++ b/internal/database/sqlite.go @@ -38,7 +38,7 @@ func InitDB(db *sql.DB) { } // Optional: Insert sample data if the table is empty - row := db.QueryRow("SELECT COUNT(*) FROM quotes") + row := db.QueryRow("SELECT COUNT(*) FROM quotes_rows") var count int if err := row.Scan(&count); err != nil { log.Fatalf("Failed to count rows: %v", err) diff --git a/internal/models/quote.go b/internal/models/quote.go index 037b06e..e6f0705 100644 --- a/internal/models/quote.go +++ b/internal/models/quote.go @@ -36,7 +36,7 @@ func init() { func GetQuoteLanguages(ctx context.Context, db *sql.DB) ([]LanguageCount, error) { query := ` SELECT language, COUNT(*) as count - FROM quotes + FROM quotes_rows GROUP BY language ` @@ -71,7 +71,7 @@ func GetAllQuotes(ctx context.Context, db *sql.DB, language string) ([]Quote, er language = "en" } - query := "SELECT id, quote, author, author_occupation FROM quotes WHERE language = ?" + query := "SELECT id, quote, author, author_occupation FROM quotes_rows WHERE language = ?" rows, err := db.QueryContext(ctx, query, language) if err != nil { log.Printf("Error querying all quotes: %v", err) @@ -104,7 +104,7 @@ func GetQuoteByID(ctx context.Context, db *sql.DB, id string, language string) ( } var quote Quote - query := "SELECT id, quote, author, author_occupation FROM quotes WHERE id = ? AND language = ?" + query := "SELECT id, quote, author, author_occupation FROM quotes_rows WHERE id = ? AND language = ?" err := db.QueryRowContext(ctx, query, id, language).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -125,7 +125,7 @@ func GetRandomQuote(ctx context.Context, db *sql.DB, language string) (Quote, er } var quote Quote - query := "SELECT id, quote, author, author_occupation FROM quotes WHERE language = ? ORDER BY RANDOM() LIMIT 1" + query := "SELECT id, quote, author, author_occupation FROM quotes_rows WHERE language = ? ORDER BY RANDOM() LIMIT 1" err := db.QueryRowContext(ctx, query, language).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -148,7 +148,7 @@ func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, language string, e query := fmt.Sprintf(` SELECT id, quote, author, author_occupation - FROM quotes + FROM quotes_rows WHERE language = ? %s ORDER BY RANDOM() LIMIT 1 From 501ca23cd3805a518c6718abd9eb066ba59ccbdd Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 28 Aug 2024 19:32:02 +0100 Subject: [PATCH 04/43] feat(images): /images, / images/photographers routes --- internal/database/sqlite.go | 78 ++------------------- internal/handlers/image_handler.go | 37 ++++++++++ internal/models/image.go | 107 +++++++++++++++++++++++++++++ main.go | 45 ++++++++---- 4 files changed, 181 insertions(+), 86 deletions(-) create mode 100644 internal/handlers/image_handler.go create mode 100644 internal/models/image.go diff --git a/internal/database/sqlite.go b/internal/database/sqlite.go index 1e92138..0a5e11b 100644 --- a/internal/database/sqlite.go +++ b/internal/database/sqlite.go @@ -4,7 +4,6 @@ import ( "database/sql" "log" - "github.com/google/uuid" _ "github.com/mattn/go-sqlite3" ) @@ -22,80 +21,11 @@ func ConnectDB(dbPath string) (*sql.DB, error) { return db, nil } +// InitDB ensures the connection to the database is established func InitDB(db *sql.DB) { - createTableQuery := ` - CREATE TABLE IF NOT EXISTS quotes ( - id TEXT PRIMARY KEY, - quote TEXT NOT NULL, - author TEXT NOT NULL, - author_occupation TEXT NOT NULL, - language TEXT NOT NULL DEFAULT 'en' - );` - - _, err := db.Exec(createTableQuery) - if err != nil { - log.Fatalf("Failed to create table: %v", err) - } - - // Optional: Insert sample data if the table is empty - row := db.QueryRow("SELECT COUNT(*) FROM quotes_rows") - var count int - if err := row.Scan(&count); err != nil { - log.Fatalf("Failed to count rows: %v", err) - } - - if count == 0 { - insertSampleData(db) - } -} - -func insertSampleData(db *sql.DB) { - insertQuery := ` - INSERT INTO quotes (id, quote, author, author_occupation, language) VALUES - (?, ?, ?, ?, ?), - (?, ?, ?, ?, ?), - (?, ?, ?, ?, ?), - (?, ?, ?, ?, ?), - (?, ?, ?, ?, ?), - (?, ?, ?, ?, ?), - (?, ?, ?, ?, ?), - (?, ?, ?, ?, ?), - (?, ?, ?, ?, ?), - (?, ?, ?, ?, ?);` - - _, err := db.Exec(insertQuery, - uuid.New().String(), "The only limit to our realization of tomorrow is our doubts of today.", "Franklin D. Roosevelt", "32nd U.S. President", "en", - uuid.New().String(), "In the end, we will remember not the words of our enemies, but the silence of our friends.", "Martin Luther King Jr.", "Civil Rights Leader", "en", - uuid.New().String(), "The greatest glory in living lies not in never falling, but in rising every time we fall.", "Nelson Mandela", "Former President of South Africa", "en", - uuid.New().String(), "La única limitación para nuestra realización del mañana son nuestras dudas de hoy.", "Franklin D. Roosevelt", "32º Presidente de los EE.UU.", "es", - uuid.New().String(), "Al final, no recordaremos las palabras de nuestros enemigos, sino el silencio de nuestros amigos.", "Martin Luther King Jr.", "Líder de los Derechos Civiles", "es", - uuid.New().String(), "La mayor gloria en la vida no radica en no caer nunca, sino en levantarnos cada vez que caemos.", "Nelson Mandela", "Ex Presidente de Sudáfrica", "es", - uuid.New().String(), "La seule limite à notre réalisation de demain est nos doutes d'aujourd'hui.", "Franklin D. Roosevelt", "32e Président des États-Unis", "fr", - uuid.New().String(), "À la fin, nous nous souviendrons non pas des mots de nos ennemis, mais du silence de nos amis.", "Martin Luther King Jr.", "Leader des droits civiques", "fr", - uuid.New().String(), "La plus grande gloire de vivre ne réside pas dans le fait de ne jamais tomber, mais de se relever à chaque fois que nous tombons.", "Nelson Mandela", "Ancien Président de l'Afrique du Sud", "fr", - uuid.New().String(), "Die einzige Grenze zu unserer Verwirklichung von morgen sind unsere Zweifel von heute.", "Franklin D. Roosevelt", "32. Präsident der USA", "de", - uuid.New().String(), "Am Ende werden wir uns nicht an die Worte unserer Feinde erinnern, sondern an das Schweigen unserer Freunde.", "Martin Luther King Jr.", "Bürgerrechtsführer", "de", - uuid.New().String(), "Der größte Ruhm im Leben liegt nicht darin, niemals zu fallen, sondern jedes Mal aufzustehen, wenn wir fallen.", "Nelson Mandela", "Ehemaliger Präsident von Südafrika", "de", - uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", - uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", - uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", - uuid.New().String(), "La única limitación para nuestra realización del mañana son nuestras dudas de hoy.", "Franklin D. Roosevelt", "32º Presidente de los EE.UU.", "es", - uuid.New().String(), "Al final, no recordaremos las palabras de nuestros enemigos, sino el silencio de nuestros amigos.", "Martin Luther King Jr.", "Líder de los Derechos Civiles", "es", - uuid.New().String(), "La mayor gloria en la vida no radica en no caer nunca, sino en levantarnos cada vez que caemos.", "Nelson Mandela", "Ex Presidente de Sudáfrica", "es", - uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", - uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", - uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", - uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", - uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", - uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", - uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", - uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", - uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", - uuid.New().String(), "唯一限制我们实现明天的,是我们今天的疑虑。", "富兰克林·D·罗斯福", "第32任美国总统", "zh", - uuid.New().String(), "最终,我们不会记得敌人的话,而是朋友的沉默。", "马丁·路德·金", "民权领袖", "zh", - uuid.New().String(), "生活中最大的荣耀不是永不跌倒,而是每次跌倒后再度站起来。", "纳尔逊·曼德拉", "前南非总统", "zh", - ) + // Check if the connection to the database is established + err := db.Ping() if err != nil { - log.Fatalf("Failed to insert sample data: %v", err) + log.Fatalf("Failed to connect to database: %v", err) } } diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go new file mode 100644 index 0000000..f410c11 --- /dev/null +++ b/internal/handlers/image_handler.go @@ -0,0 +1,37 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "net/http" + + "quote-api/internal/models" +) + +type ImageHandler struct { + DB *sql.DB +} + +func (h *ImageHandler) GetImagePhotographers(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + photographers, err := models.GetImagePhotographers(ctx, h.DB) + if err != nil { + http.Error(w, "Failed to get photographers", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(photographers) +} + +func (h *ImageHandler) GetImages(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + photographer := r.URL.Query().Get("photographer") + category := r.URL.Query().Get("category") + images, err := models.GetImages(ctx, h.DB, photographer, category) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(images) +} diff --git a/internal/models/image.go b/internal/models/image.go new file mode 100644 index 0000000..a7266ce --- /dev/null +++ b/internal/models/image.go @@ -0,0 +1,107 @@ +package models + +import ( + "context" + "database/sql" + "log" +) + +type Image struct { + ID string + Camera *string + CreatedAt *string + LocationData *string + Photographer string + Category string + OriginalFileName string + Colour string + PUN int + Version int + BlurHash string +} + +type PhotographerCount struct { + Photographer string + Count int +} + +func GetImagePhotographers(ctx context.Context, db *sql.DB) ([]PhotographerCount, error) { + query := ` + SELECT photographer, COUNT(*) as count + FROM images_rows + GROUP BY photographer + ` + + rows, err := db.QueryContext(ctx, query) + if err != nil { + log.Printf("Error querying image photographers: %v", err) + return nil, err + } + defer rows.Close() + + var photographers []PhotographerCount + for rows.Next() { + var lc PhotographerCount + if err := rows.Scan(&lc.Photographer, &lc.Count); err != nil { + log.Printf("Error scanning photographer count: %v", err) + return nil, err + } + photographers = append(photographers, lc) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return photographers, nil +} + +func GetImages(ctx context.Context, db *sql.DB, photographer, category string) ([]Image, error) { + query := ` + SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash + FROM images_rows + ` + var args []interface{} + var conditions []string + + if photographer != "" { + conditions = append(conditions, "photographer = ?") + args = append(args, photographer) + } + if category != "" { + conditions = append(conditions, "category = ?") + args = append(args, category) + } + + if len(conditions) > 0 { + query += " WHERE " + conditions[0] + for i := 1; i < len(conditions); i++ { + query += " AND " + conditions[i] + } + } + + rows, err := db.QueryContext(ctx, query, args...) + if err != nil { + log.Printf("Error querying images: %v", err) + return nil, err + } + defer rows.Close() + + var images []Image + for rows.Next() { + var img Image + if err := rows.Scan(&img.ID, &img.Camera, &img.CreatedAt, &img.LocationData, &img.Photographer, &img.Category, &img.OriginalFileName, &img.Colour, &img.PUN, &img.Version, &img.BlurHash); err != nil { + log.Printf("Error scanning image: %v", err) + return nil, err + } + images = append(images, img) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return images, nil +} diff --git a/main.go b/main.go index acf23ef..2b775fc 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,10 @@ package main import ( + "database/sql" "log" "net/http" - "net/http/pprof" // Import pprof + "net/http/pprof" "os" "quote-api/internal/database" @@ -14,27 +15,40 @@ import ( "github.com/joho/godotenv" ) -func main() { - // Load environment variables from .env file - err := godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } - - dbPath := os.Getenv("DB_PATH") +// loadAndConnectDB loads environment variables and connects to the database +func loadAndConnectDB(envVar string) *sql.DB { + dbPath := os.Getenv(envVar) if dbPath == "" { - log.Fatal("DB_PATH environment variable is not set") + log.Fatalf("%s environment variable is not set", envVar) } db, err := database.ConnectDB(dbPath) if err != nil { log.Fatalf("Failed to connect to database: %v", err) } - defer db.Close() database.InitDB(db) + return db +} + +func main() { + // Load environment variables from .env file + err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } + + // Connect to quotes_db + quotesDB := loadAndConnectDB("QUOTES_DB_PATH") + log.Println(os.Getenv("QUOTES_DB_PATH")) + defer quotesDB.Close() - quoteHandler := &handlers.QuoteHandler{DB: db} + // Connect to images_db + imagesDB := loadAndConnectDB("IMAGES_DB_PATH") + defer imagesDB.Close() + + quoteHandler := &handlers.QuoteHandler{DB: quotesDB} + imageHandler := &handlers.ImageHandler{DB: imagesDB} r := chi.NewRouter() @@ -45,11 +59,18 @@ func main() { // Routes r.Get("/", handlers.RootHandler) + // Quote routes r.Get("/quotes", quoteHandler.GetAllQuotes) r.Get("/quotes/{id}", quoteHandler.GetQuoteByID) r.Get("/quotes/random", quoteHandler.GetRandomQuote) r.Get("/quotes/languages", quoteHandler.GetQuoteLanguages) + // Image routes + r.Get("/images", imageHandler.GetImages) + // r.Get("/images/{id}", imageHandler.GetImageByID) + // r.Get("/images/random", imageHandler.GetRandomImage) + r.Get("/images/photographers", imageHandler.GetImagePhotographers) + // pprof routes r.HandleFunc("/debug/pprof/*", pprof.Index) r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) From 34559f72505e51b1fa5e89e9593854fec1f1edbf Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 28 Aug 2024 20:55:17 +0100 Subject: [PATCH 05/43] feat: better logging for database connection --- qodana.yaml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 qodana.yaml diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..215d808 --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,29 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-go:latest From a7fdf536dcc09f2c79d6b5e4583d0fe832570863 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 28 Aug 2024 20:56:51 +0100 Subject: [PATCH 06/43] chore: Add .zip files to .gitignore and improve logging for database connection --- .gitignore | 1 + internal/models/quote.go | 4 ++-- main.go | 3 ++- script.js | 20 ++++++++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 script.js diff --git a/.gitignore b/.gitignore index a7aa63c..0b8a2b8 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out *.db +*.zip # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/internal/models/quote.go b/internal/models/quote.go index e6f0705..dbcf8eb 100644 --- a/internal/models/quote.go +++ b/internal/models/quote.go @@ -15,8 +15,8 @@ import ( type Quote struct { ID string Quote string - Author string - Occupation string + Author *string + Occupation *string } // LanguageCount represents a language and its quote count. diff --git a/main.go b/main.go index 2b775fc..254b400 100644 --- a/main.go +++ b/main.go @@ -40,11 +40,12 @@ func main() { // Connect to quotes_db quotesDB := loadAndConnectDB("QUOTES_DB_PATH") - log.Println(os.Getenv("QUOTES_DB_PATH")) + log.Println("Connected to quotes_db") defer quotesDB.Close() // Connect to images_db imagesDB := loadAndConnectDB("IMAGES_DB_PATH") + log.Println("Connected to images_db") defer imagesDB.Close() quoteHandler := &handlers.QuoteHandler{DB: quotesDB} diff --git a/script.js b/script.js new file mode 100644 index 0000000..b6e7887 --- /dev/null +++ b/script.js @@ -0,0 +1,20 @@ +import http from "k6/http"; +import { sleep } from "k6"; + +export let options = { + vus: 25000, // virtual users + duration: "10s", // duration of the test +}; + +export default function () { + let res = http.get("http://localhost:8080/quotes/random"); + + // Log the response body to the console + console.log("Response body: " + res.body); + + // Optional: Log the status code to the console + console.log("Status code: " + res.status); + + // Sleep for 1 second between requests + sleep(1); +} From 105c25874a685dfb1ea3430ec59ffcafe1b3e3e0 Mon Sep 17 00:00:00 2001 From: Jack Date: Wed, 28 Aug 2024 21:33:58 +0100 Subject: [PATCH 07/43] docs(image_handler): Pointed out where the code ends --- internal/handlers/image_handler.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index f410c11..8e8e4f8 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -35,3 +35,5 @@ func (h *ImageHandler) GetImages(w http.ResponseWriter, r *http.Request) { } json.NewEncoder(w).Encode(images) } + +// End of code From 05b25d4cef8a1f85f636bf9b6f241604a4ceee12 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 28 Aug 2024 23:47:21 +0100 Subject: [PATCH 08/43] feat(image): GET Image by ID --- .gitignore | 1 + internal/handlers/image_handler.go | 13 +++++++++++++ internal/handlers/quote_handler.go | 3 +-- internal/handlers/root_handler.go | 28 +++++++++++++++++++--------- internal/models/image.go | 23 ++++++++++++++++++++--- internal/models/quote.go | 12 ++++-------- main.go | 2 +- 7 files changed, 59 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 0b8a2b8..3b77ffb 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ *.out *.db *.zip +*.DS_Store # Dependency directories (remove the comment below to include it) # vendor/ diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index 8e8e4f8..22fc6b2 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -6,6 +6,8 @@ import ( "net/http" "quote-api/internal/models" + + "github.com/go-chi/chi/v5" ) type ImageHandler struct { @@ -24,6 +26,17 @@ func (h *ImageHandler) GetImagePhotographers(w http.ResponseWriter, r *http.Requ json.NewEncoder(w).Encode(photographers) } +func (h *ImageHandler) GetImageByID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + id := chi.URLParam(r, "id") + image, err := models.GetImageByID(ctx, h.DB, id) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(image) +} + func (h *ImageHandler) GetImages(w http.ResponseWriter, r *http.Request) { ctx := r.Context() photographer := r.URL.Query().Get("photographer") diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go index 94ccb37..c6766cf 100644 --- a/internal/handlers/quote_handler.go +++ b/internal/handlers/quote_handler.go @@ -42,8 +42,7 @@ func (h *QuoteHandler) GetAllQuotes(w http.ResponseWriter, r *http.Request) { func (h *QuoteHandler) GetQuoteByID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id := chi.URLParam(r, "id") - language := r.URL.Query().Get("language") - quote, err := models.GetQuoteByID(ctx, h.DB, id, language) + quote, err := models.GetQuoteByID(ctx, h.DB, id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/handlers/root_handler.go b/internal/handlers/root_handler.go index 2430d57..24da56a 100644 --- a/internal/handlers/root_handler.go +++ b/internal/handlers/root_handler.go @@ -15,24 +15,34 @@ type ApiInfo struct { Name string `json:"name"` Version string `json:"version"` Description string `json:"description"` - Endpoints []Endpoint `json:"endpoints"` + QuoteEndpoints []Endpoint `json:"quote_endpoints"` + ImageEndpoints []Endpoint `json:"image_endpoints"` DocumentationURL string `json:"documentation_url"` } // rootHandler handles requests to the root endpoint. func RootHandler(w http.ResponseWriter, r *http.Request) { apiInfo := ApiInfo{ - Name: "Quote API", - Version: "1.0.0", - Description: "A simple API for fetching inspirational quotes.", - Endpoints: []Endpoint{ - {Method: "GET", Path: "/quotes", Description: "Retrieve a list of all quotes"}, - {Method: "GET", Path: "/quotes/{id}", Description: "Retrieve a quote by its ID"}, - {Method: "GET", Path: "/quotes/random", Description: "Retrieve a random quote"}, + Name: "Mue API", + Version: "0.0.1", + Description: "This API provides quotes and images for the Mue Tab browser extension.", + QuoteEndpoints: []Endpoint{ + {Method: "GET", Path: "/quotes", Description: "Retrieve all quotes."}, + {Method: "GET", Path: "/quotes/{id}", Description: "Retrieve a specific quote by ID."}, + {Method: "GET", Path: "/quotes/random", Description: "Retrieve a random quote."}, + {Method: "GET", Path: "/quotes/language", Description: "Retrieve all available languages and the count of quotes in each."}, + }, + ImageEndpoints: []Endpoint{ + {Method: "GET", Path: "/images", Description: "Retrieve all images."}, + {Method: "GET", Path: "/images/{id}", Description: "Retrieve a specific image by ID."}, + {Method: "GET", Path: "/images/random", Description: "Retrieve a random image."}, + {Method: "GET", Path: "/images/photographers", Description: "Retrieve all photographers and the count of images by each."}, }, DocumentationURL: "https://docs.muetab.com/", } w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(apiInfo) + if err := json.NewEncoder(w).Encode(apiInfo); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } } diff --git a/internal/models/image.go b/internal/models/image.go index a7266ce..7fcdb3b 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -3,6 +3,7 @@ package models import ( "context" "database/sql" + "errors" "log" ) @@ -57,6 +58,22 @@ func GetImagePhotographers(ctx context.Context, db *sql.DB) ([]PhotographerCount return photographers, nil } +func GetImageByID(ctx context.Context, db *sql.DB, id string) (Image, error) { + var image Image + query := "SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash FROM images_rows WHERE id = ?" + err := db.QueryRowContext(ctx, query, id).Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + log.Printf("No image found with id %s", id) + return image, errors.New("no image found") + } + log.Printf("Error querying imag by ID: %v", err) + return image, err + } + + return image, nil +} + func GetImages(ctx context.Context, db *sql.DB, photographer, category string) ([]Image, error) { query := ` SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash @@ -90,12 +107,12 @@ func GetImages(ctx context.Context, db *sql.DB, photographer, category string) ( var images []Image for rows.Next() { - var img Image - if err := rows.Scan(&img.ID, &img.Camera, &img.CreatedAt, &img.LocationData, &img.Photographer, &img.Category, &img.OriginalFileName, &img.Colour, &img.PUN, &img.Version, &img.BlurHash); err != nil { + var image Image + if err := rows.Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash); err != nil { log.Printf("Error scanning image: %v", err) return nil, err } - images = append(images, img) + images = append(images, image) } if err = rows.Err(); err != nil { diff --git a/internal/models/quote.go b/internal/models/quote.go index dbcf8eb..86faa00 100644 --- a/internal/models/quote.go +++ b/internal/models/quote.go @@ -98,17 +98,13 @@ func GetAllQuotes(ctx context.Context, db *sql.DB, language string) ([]Quote, er } // GetQuoteByID fetches a quote by its ID in the specified language. -func GetQuoteByID(ctx context.Context, db *sql.DB, id string, language string) (Quote, error) { - if language == "" { - language = "en" - } - +func GetQuoteByID(ctx context.Context, db *sql.DB, id string) (Quote, error) { var quote Quote - query := "SELECT id, quote, author, author_occupation FROM quotes_rows WHERE id = ? AND language = ?" - err := db.QueryRowContext(ctx, query, id, language).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) + query := "SELECT id, quote, author, author_occupation FROM quotes_rows WHERE id = ?" + err := db.QueryRowContext(ctx, query, id).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) if err != nil { if errors.Is(err, sql.ErrNoRows) { - log.Printf("No quote found with id %s and language %s", id, language) + log.Printf("No quote found with id %s", id) return quote, errors.New("no quote found") } log.Printf("Error querying quote by ID: %v", err) diff --git a/main.go b/main.go index 254b400..101f1fb 100644 --- a/main.go +++ b/main.go @@ -68,7 +68,7 @@ func main() { // Image routes r.Get("/images", imageHandler.GetImages) - // r.Get("/images/{id}", imageHandler.GetImageByID) + r.Get("/images/{id}", imageHandler.GetImageByID) // r.Get("/images/random", imageHandler.GetRandomImage) r.Get("/images/photographers", imageHandler.GetImagePhotographers) From dbdd30fd9c054ee05bf1fc43a0b7bb42d0e68bff Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 28 Aug 2024 23:50:44 +0100 Subject: [PATCH 09/43] refactor: rename from quote-api --- go.mod | 2 +- internal/handlers/image_handler.go | 2 +- internal/handlers/quote_handler.go | 2 +- main.go | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index d964afe..ba2a6cd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module quote-api +module mue-api go 1.23.0 diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index 22fc6b2..e94513b 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -5,7 +5,7 @@ import ( "encoding/json" "net/http" - "quote-api/internal/models" + "mue-api/internal/models" "github.com/go-chi/chi/v5" ) diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go index c6766cf..7b38234 100644 --- a/internal/handlers/quote_handler.go +++ b/internal/handlers/quote_handler.go @@ -7,7 +7,7 @@ import ( "net/http" "strings" - "quote-api/internal/models" + "mue-api/internal/models" "github.com/go-chi/chi/v5" ) diff --git a/main.go b/main.go index 101f1fb..c36f885 100644 --- a/main.go +++ b/main.go @@ -7,8 +7,8 @@ import ( "net/http/pprof" "os" - "quote-api/internal/database" - "quote-api/internal/handlers" + "mue-api/internal/database" + "mue-api/internal/handlers" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" From 011da2d1b24aae9f0a5b33ea26672dd9770c8ecd Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Thu, 29 Aug 2024 23:35:45 +0100 Subject: [PATCH 10/43] feat(images): Sizes & Categories endpoints --- internal/handlers/image_handler.go | 31 +++++++++++++++++++++++++ internal/handlers/root_handler.go | 2 ++ internal/models/image.go | 37 ++++++++++++++++++++++++++++++ main.go | 2 ++ 4 files changed, 72 insertions(+) diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index e94513b..dbe086e 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -14,6 +14,11 @@ type ImageHandler struct { DB *sql.DB } +type Sizes struct { + ID string `json:"id"` + Label string `json:"label"` +} + func (h *ImageHandler) GetImagePhotographers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() photographers, err := models.GetImagePhotographers(ctx, h.DB) @@ -49,4 +54,30 @@ func (h *ImageHandler) GetImages(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(images) } +func (h *ImageHandler) GetImageCategories(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + categories, err := models.GetImageCategories(ctx, h.DB) + if err != nil { + http.Error(w, "Failed to get categories", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(categories) +} + +func (h *ImageHandler) GetImageSizes(w http.ResponseWriter, r *http.Request) { + sizes := []Sizes{ + {ID: "original", Label: "original"}, + {ID: "qhd", Label: "high"}, + {ID: "fhd", Label: "normal"}, + {ID: "hd", Label: "datasaver"}, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(sizes); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + // End of code diff --git a/internal/handlers/root_handler.go b/internal/handlers/root_handler.go index 24da56a..e5668f7 100644 --- a/internal/handlers/root_handler.go +++ b/internal/handlers/root_handler.go @@ -37,6 +37,8 @@ func RootHandler(w http.ResponseWriter, r *http.Request) { {Method: "GET", Path: "/images/{id}", Description: "Retrieve a specific image by ID."}, {Method: "GET", Path: "/images/random", Description: "Retrieve a random image."}, {Method: "GET", Path: "/images/photographers", Description: "Retrieve all photographers and the count of images by each."}, + {Method: "GET", Path: "/images/categories", Description: "Retrieve all image categories."}, + {Method: "GET", Path: "/images/sizes", Description: "Retrieve all image sizes."}, }, DocumentationURL: "https://docs.muetab.com/", } diff --git a/internal/models/image.go b/internal/models/image.go index 7fcdb3b..db42da0 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -26,6 +26,11 @@ type PhotographerCount struct { Count int } +type CategoryCount struct { + Category string + Count int +} + func GetImagePhotographers(ctx context.Context, db *sql.DB) ([]PhotographerCount, error) { query := ` SELECT photographer, COUNT(*) as count @@ -58,6 +63,38 @@ func GetImagePhotographers(ctx context.Context, db *sql.DB) ([]PhotographerCount return photographers, nil } +func GetImageCategories(ctx context.Context, db *sql.DB) ([]CategoryCount, error) { + query := ` + SELECT category, COUNT(*) as count + FROM images_rows + GROUP BY category +` + + rows, err := db.QueryContext(ctx, query) + if err != nil { + log.Printf("Error querying image categories: %v", err) + return nil, err + } + defer rows.Close() + + var categories []CategoryCount + for rows.Next() { + var lc CategoryCount + if err := rows.Scan(&lc.Category, &lc.Count); err != nil { + log.Printf("Error scanning category count: %v", err) + return nil, err + } + categories = append(categories, lc) + } + + if err = rows.Err(); err != nil { + log.Printf("Error iterating over rows: %v", err) + return nil, err + } + + return categories, nil +} + func GetImageByID(ctx context.Context, db *sql.DB, id string) (Image, error) { var image Image query := "SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash FROM images_rows WHERE id = ?" diff --git a/main.go b/main.go index c36f885..015f4e6 100644 --- a/main.go +++ b/main.go @@ -71,6 +71,8 @@ func main() { r.Get("/images/{id}", imageHandler.GetImageByID) // r.Get("/images/random", imageHandler.GetRandomImage) r.Get("/images/photographers", imageHandler.GetImagePhotographers) + r.Get("/images/sizes", imageHandler.GetImageSizes) + r.Get("/images/categories", imageHandler.GetImageCategories) // pprof routes r.HandleFunc("/debug/pprof/*", pprof.Index) From d4f20abfb91db56cfc2d272be836d723fab9b473 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Thu, 29 Aug 2024 23:51:30 +0100 Subject: [PATCH 11/43] feat: graceful shutdown --- go.mod | 6 +- go.sum | 11 +++ internal/handlers/quote_handler.go | 7 -- main.go | 112 +++++++++++++++++++++++------ 4 files changed, 106 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index ba2a6cd..0e1305e 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,8 @@ require ( github.com/mattn/go-sqlite3 v1.14.22 ) -require github.com/joho/godotenv v1.5.1 // indirect +require ( + github.com/joho/godotenv v1.5.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect +) diff --git a/go.sum b/go.sum index 0b19ca1..90be919 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -6,3 +8,12 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go index 7b38234..b78c8fc 100644 --- a/internal/handlers/quote_handler.go +++ b/internal/handlers/quote_handler.go @@ -101,10 +101,6 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { seenQuotes = append(seenQuotes, quote.ID) } - // Debugging: Log the updated list of seen quotes - log.Printf("Selected quote ID: %s", quote.ID) - log.Printf("Updated seen quotes list: %s", strings.Join(seenQuotes, ",")) - // Update the cookie with the new list of seen quote IDs seenQuotesStr := strings.Join(seenQuotes, ",") newCookie := &http.Cookie{ @@ -114,9 +110,6 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { // Consider setting MaxAge or Expires if you want the cookie to persist across sessions } - // Debugging: Log the cookie value before setting it - log.Printf("Setting seen_quotes cookie with value: %s", seenQuotesStr) - http.SetCookie(w, newCookie) // Return the quote as JSON diff --git a/main.go b/main.go index 015f4e6..acf9163 100644 --- a/main.go +++ b/main.go @@ -1,64 +1,102 @@ package main import ( + "context" "database/sql" "log" "net/http" "net/http/pprof" "os" - - "mue-api/internal/database" - "mue-api/internal/handlers" + "os/signal" + "syscall" + "time" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/joho/godotenv" + "github.com/sirupsen/logrus" + + "mue-api/internal/database" + "mue-api/internal/handlers" ) +// Config holds the application configuration +type Config struct { + QuotesDBPath string + ImagesDBPath string + ServerPort string +} + +// loadConfig loads configuration from environment variables +func loadConfig() *Config { + return &Config{ + QuotesDBPath: getEnv("QUOTES_DB_PATH", "default_quotes_db_path"), + ImagesDBPath: getEnv("IMAGES_DB_PATH", "default_images_db_path"), + ServerPort: getEnv("SERVER_PORT", "8080"), + } +} + +// getEnv retrieves an environment variable or returns a fallback value +func getEnv(key, fallback string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return fallback +} + // loadAndConnectDB loads environment variables and connects to the database -func loadAndConnectDB(envVar string) *sql.DB { - dbPath := os.Getenv(envVar) +func loadAndConnectDB(dbPath string) *sql.DB { + log := logrus.New() + if dbPath == "" { - log.Fatalf("%s environment variable is not set", envVar) + log.Fatalf("Database path is not set") } db, err := database.ConnectDB(dbPath) if err != nil { - log.Fatalf("Failed to connect to database: %v", err) + log.Fatalf("Failed to connect to database %s: %v", dbPath, err) } + log.WithFields(logrus.Fields{ + "dbPath": dbPath, + }).Info("Connected to database") + database.InitDB(db) return db } +// main is the entry point of the application func main() { // Load environment variables from .env file err := godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } + checkErr(err, "Error loading .env file") + + // Load the configuration + config := loadConfig() // Connect to quotes_db - quotesDB := loadAndConnectDB("QUOTES_DB_PATH") - log.Println("Connected to quotes_db") + quotesDB := loadAndConnectDB(config.QuotesDBPath) defer quotesDB.Close() // Connect to images_db - imagesDB := loadAndConnectDB("IMAGES_DB_PATH") - log.Println("Connected to images_db") + imagesDB := loadAndConnectDB(config.ImagesDBPath) defer imagesDB.Close() + // Initialize handlers quoteHandler := &handlers.QuoteHandler{DB: quotesDB} imageHandler := &handlers.ImageHandler{DB: imagesDB} + // Setup router and middleware r := chi.NewRouter() - - // Middleware r.Use(middleware.Logger) r.Use(middleware.Recoverer) - // Routes + // Define routes r.Get("/", handlers.RootHandler) + r.Get("/health", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("OK")) + }) // Quote routes r.Get("/quotes", quoteHandler.GetAllQuotes) @@ -69,21 +107,51 @@ func main() { // Image routes r.Get("/images", imageHandler.GetImages) r.Get("/images/{id}", imageHandler.GetImageByID) - // r.Get("/images/random", imageHandler.GetRandomImage) r.Get("/images/photographers", imageHandler.GetImagePhotographers) + //r.Get("/images/random", imageHandler.GetRandomImage) r.Get("/images/sizes", imageHandler.GetImageSizes) r.Get("/images/categories", imageHandler.GetImageCategories) // pprof routes - r.HandleFunc("/debug/pprof/*", pprof.Index) + r.HandleFunc("/debug/pprof/", pprof.Index) r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) r.HandleFunc("/debug/pprof/profile", pprof.Profile) r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) r.HandleFunc("/debug/pprof/trace", pprof.Trace) - log.Println("Starting server on :8080") - err = http.ListenAndServe(":8080", r) + // Set up the server + srv := &http.Server{ + Addr: ":" + config.ServerPort, + Handler: r, + } + + // Graceful shutdown + go func() { + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("Failed to start server: %v", err) + } + }() + log.Printf("Server started on :%s", config.ServerPort) + + // Wait for interrupt signal to gracefully shutdown the server + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exiting") +} + +// checkErr logs a fatal error if err is non-nil +func checkErr(err error, message string) { if err != nil { - log.Fatalf("Failed to start server: %v", err) + log.Fatalf("%s: %v", message, err) } } From 027b10cf18726963f1ef21fbdf9424bca533ccd8 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Fri, 30 Aug 2024 21:27:11 +0100 Subject: [PATCH 12/43] refactor: introduce utils package for more readable code --- internal/middleware/nocookie.go | 23 +++++ .../{database/sqlite.go => utils/database.go} | 24 +++++- internal/utils/env.go | 11 +++ main.go | 85 ++++++++----------- 4 files changed, 92 insertions(+), 51 deletions(-) create mode 100644 internal/middleware/nocookie.go rename internal/{database/sqlite.go => utils/database.go} (54%) create mode 100644 internal/utils/env.go diff --git a/internal/middleware/nocookie.go b/internal/middleware/nocookie.go new file mode 100644 index 0000000..c99b553 --- /dev/null +++ b/internal/middleware/nocookie.go @@ -0,0 +1,23 @@ +package middleware + +import "net/http" + +func NoCookieMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Check if the 'no-cookie' query parameter is set to '1' + if r.URL.Query().Get("no-cookie") == "1" { + w = &noCookieResponseWriter{ResponseWriter: w} + r.Header.Del("Cookie") + } + next.ServeHTTP(w, r) + }) +} + +type noCookieResponseWriter struct { + http.ResponseWriter +} + +func (w *noCookieResponseWriter) WriteHeader(statusCode int) { + w.ResponseWriter.Header().Del("Set-Cookie") + w.ResponseWriter.WriteHeader(statusCode) +} diff --git a/internal/database/sqlite.go b/internal/utils/database.go similarity index 54% rename from internal/database/sqlite.go rename to internal/utils/database.go index 0a5e11b..2e60054 100644 --- a/internal/database/sqlite.go +++ b/internal/utils/database.go @@ -1,10 +1,11 @@ -package database +package utils import ( "database/sql" "log" _ "github.com/mattn/go-sqlite3" + "github.com/sirupsen/logrus" ) func ConnectDB(dbPath string) (*sql.DB, error) { @@ -29,3 +30,24 @@ func InitDB(db *sql.DB) { log.Fatalf("Failed to connect to database: %v", err) } } + +// loadAndConnectDB loads environment variables and connects to the database +func LoadAndConnectDB(dbPath string) *sql.DB { + log := logrus.New() + + if dbPath == "" { + log.Fatalf("Database path is not set") + } + + db, err := ConnectDB(dbPath) + if err != nil { + log.Fatalf("Failed to connect to database %s: %v", dbPath, err) + } + + log.WithFields(logrus.Fields{ + "dbPath": dbPath, + }).Info("Connected to database") + + InitDB(db) + return db +} diff --git a/internal/utils/env.go b/internal/utils/env.go new file mode 100644 index 0000000..342923f --- /dev/null +++ b/internal/utils/env.go @@ -0,0 +1,11 @@ +package utils + +import "os" + +// GetEnv retrieves an environment variable or returns a fallback value +func GetEnv(key, fallback string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return fallback +} diff --git a/main.go b/main.go index acf9163..295b2db 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "database/sql" "log" "net/http" "net/http/pprof" @@ -11,13 +10,13 @@ import ( "syscall" "time" + "mue-api/internal/handlers" + local_middleware "mue-api/internal/middleware" + "mue-api/internal/utils" + "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/joho/godotenv" - "github.com/sirupsen/logrus" - - "mue-api/internal/database" - "mue-api/internal/handlers" ) // Config holds the application configuration @@ -30,39 +29,10 @@ type Config struct { // loadConfig loads configuration from environment variables func loadConfig() *Config { return &Config{ - QuotesDBPath: getEnv("QUOTES_DB_PATH", "default_quotes_db_path"), - ImagesDBPath: getEnv("IMAGES_DB_PATH", "default_images_db_path"), - ServerPort: getEnv("SERVER_PORT", "8080"), - } -} - -// getEnv retrieves an environment variable or returns a fallback value -func getEnv(key, fallback string) string { - if value, exists := os.LookupEnv(key); exists { - return value + QuotesDBPath: utils.GetEnv("QUOTES_DB_PATH", "default_quotes_db_path"), + ImagesDBPath: utils.GetEnv("IMAGES_DB_PATH", "default_images_db_path"), + ServerPort: utils.GetEnv("SERVER_PORT", "8080"), } - return fallback -} - -// loadAndConnectDB loads environment variables and connects to the database -func loadAndConnectDB(dbPath string) *sql.DB { - log := logrus.New() - - if dbPath == "" { - log.Fatalf("Database path is not set") - } - - db, err := database.ConnectDB(dbPath) - if err != nil { - log.Fatalf("Failed to connect to database %s: %v", dbPath, err) - } - - log.WithFields(logrus.Fields{ - "dbPath": dbPath, - }).Info("Connected to database") - - database.InitDB(db) - return db } // main is the entry point of the application @@ -75,11 +45,11 @@ func main() { config := loadConfig() // Connect to quotes_db - quotesDB := loadAndConnectDB(config.QuotesDBPath) + quotesDB := utils.LoadAndConnectDB(config.QuotesDBPath) defer quotesDB.Close() // Connect to images_db - imagesDB := loadAndConnectDB(config.ImagesDBPath) + imagesDB := utils.LoadAndConnectDB(config.ImagesDBPath) defer imagesDB.Close() // Initialize handlers @@ -88,8 +58,11 @@ func main() { // Setup router and middleware r := chi.NewRouter() + r.Use(middleware.RequestID) + r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) + r.Use(local_middleware.NoCookieMiddleware) // Define routes r.Get("/", handlers.RootHandler) @@ -99,19 +72,31 @@ func main() { }) // Quote routes - r.Get("/quotes", quoteHandler.GetAllQuotes) - r.Get("/quotes/{id}", quoteHandler.GetQuoteByID) - r.Get("/quotes/random", quoteHandler.GetRandomQuote) - r.Get("/quotes/languages", quoteHandler.GetQuoteLanguages) + r.Route("/quotes", func(r chi.Router) { + r.Get("/", quoteHandler.GetAllQuotes) + r.Get("/random", quoteHandler.GetRandomQuote) + r.Get("/languages", quoteHandler.GetQuoteLanguages) + + r.Route("/{id}", func(r chi.Router) { + r.Get("/", quoteHandler.GetQuoteByID) + // r.Put("/", quoteHandler.UpdateQuote) + // r.Delete("/", quoteHandler.DeleteQuote) + }) + }) // Image routes - r.Get("/images", imageHandler.GetImages) - r.Get("/images/{id}", imageHandler.GetImageByID) - r.Get("/images/photographers", imageHandler.GetImagePhotographers) - //r.Get("/images/random", imageHandler.GetRandomImage) - r.Get("/images/sizes", imageHandler.GetImageSizes) - r.Get("/images/categories", imageHandler.GetImageCategories) - + r.Route("/images", func(r chi.Router) { + r.Get("/", imageHandler.GetImages) + r.Get("/photographers", imageHandler.GetImagePhotographers) + //r.Get("/random", imageHandler.GetRandomImage) + r.Get("/sizes", imageHandler.GetImageSizes) + r.Get("/categories", imageHandler.GetImageCategories) + r.Route("/{id}", func(r chi.Router) { + r.Get("/", imageHandler.GetImageByID) + // r.Put("/", imageHandler.UpdateImage) + // r.Delete("/", imageHandler.DeleteImage) + }) + }) // pprof routes r.HandleFunc("/debug/pprof/", pprof.Index) r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) From b956042d23f61cc1bf656f0abdfd3a47d5113593 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Fri, 30 Aug 2024 22:53:46 +0100 Subject: [PATCH 13/43] feat: Use Turso DB --- go.mod | 12 +++++++++--- go.sum | 16 ++++++++++++++++ internal/models/image.go | 8 ++++---- internal/models/quote.go | 10 +++++----- internal/utils/database.go | 21 ++++++++++++++------- main.go | 25 ++++++++++--------------- 6 files changed, 58 insertions(+), 34 deletions(-) diff --git a/go.mod b/go.mod index 0e1305e..f6ba113 100644 --- a/go.mod +++ b/go.mod @@ -5,11 +5,17 @@ go 1.23.0 require ( github.com/go-chi/chi/v5 v5.1.0 github.com/google/uuid v1.6.0 + github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.22 + github.com/sirupsen/logrus v1.9.3 ) require ( - github.com/joho/godotenv v1.5.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/coder/websocket v1.8.12 // indirect + github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect + github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 // indirect + github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/sys v0.1.0 // indirect ) diff --git a/go.sum b/go.sum index 90be919..4694842 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= @@ -6,6 +10,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= +github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -13,7 +19,17 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 h1:JclfLfqxOsICgvSNhF5W3cY2v5tylOtYJdTZKgPM8Bg= +github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= +github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 h1:iLjJLq2A5J6L9zrhyNn+fpmxFvtEpYB4XLMr0rX3epI= +github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/models/image.go b/internal/models/image.go index db42da0..a3d273a 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -34,7 +34,7 @@ type CategoryCount struct { func GetImagePhotographers(ctx context.Context, db *sql.DB) ([]PhotographerCount, error) { query := ` SELECT photographer, COUNT(*) as count - FROM images_rows + FROM images GROUP BY photographer ` @@ -66,7 +66,7 @@ func GetImagePhotographers(ctx context.Context, db *sql.DB) ([]PhotographerCount func GetImageCategories(ctx context.Context, db *sql.DB) ([]CategoryCount, error) { query := ` SELECT category, COUNT(*) as count - FROM images_rows + FROM images GROUP BY category ` @@ -97,7 +97,7 @@ func GetImageCategories(ctx context.Context, db *sql.DB) ([]CategoryCount, error func GetImageByID(ctx context.Context, db *sql.DB, id string) (Image, error) { var image Image - query := "SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash FROM images_rows WHERE id = ?" + query := "SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash FROM images WHERE id = ?" err := db.QueryRowContext(ctx, query, id).Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -114,7 +114,7 @@ func GetImageByID(ctx context.Context, db *sql.DB, id string) (Image, error) { func GetImages(ctx context.Context, db *sql.DB, photographer, category string) ([]Image, error) { query := ` SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash - FROM images_rows + FROM images ` var args []interface{} var conditions []string diff --git a/internal/models/quote.go b/internal/models/quote.go index 86faa00..53ee241 100644 --- a/internal/models/quote.go +++ b/internal/models/quote.go @@ -36,7 +36,7 @@ func init() { func GetQuoteLanguages(ctx context.Context, db *sql.DB) ([]LanguageCount, error) { query := ` SELECT language, COUNT(*) as count - FROM quotes_rows + FROM quotes GROUP BY language ` @@ -71,7 +71,7 @@ func GetAllQuotes(ctx context.Context, db *sql.DB, language string) ([]Quote, er language = "en" } - query := "SELECT id, quote, author, author_occupation FROM quotes_rows WHERE language = ?" + query := "SELECT id, quote, author, author_occupation FROM quotes WHERE language = ?" rows, err := db.QueryContext(ctx, query, language) if err != nil { log.Printf("Error querying all quotes: %v", err) @@ -100,7 +100,7 @@ func GetAllQuotes(ctx context.Context, db *sql.DB, language string) ([]Quote, er // GetQuoteByID fetches a quote by its ID in the specified language. func GetQuoteByID(ctx context.Context, db *sql.DB, id string) (Quote, error) { var quote Quote - query := "SELECT id, quote, author, author_occupation FROM quotes_rows WHERE id = ?" + query := "SELECT id, quote, author, author_occupation FROM quotes WHERE id = ?" err := db.QueryRowContext(ctx, query, id).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -121,7 +121,7 @@ func GetRandomQuote(ctx context.Context, db *sql.DB, language string) (Quote, er } var quote Quote - query := "SELECT id, quote, author, author_occupation FROM quotes_rows WHERE language = ? ORDER BY RANDOM() LIMIT 1" + query := "SELECT id, quote, author, author_occupation FROM quotes WHERE language = ? ORDER BY RANDOM() LIMIT 1" err := db.QueryRowContext(ctx, query, language).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -144,7 +144,7 @@ func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, language string, e query := fmt.Sprintf(` SELECT id, quote, author, author_occupation - FROM quotes_rows + FROM quotes WHERE language = ? %s ORDER BY RANDOM() LIMIT 1 diff --git a/internal/utils/database.go b/internal/utils/database.go index 2e60054..3dca079 100644 --- a/internal/utils/database.go +++ b/internal/utils/database.go @@ -6,10 +6,13 @@ import ( _ "github.com/mattn/go-sqlite3" "github.com/sirupsen/logrus" + _ "github.com/tursodatabase/libsql-client-go/libsql" ) -func ConnectDB(dbPath string) (*sql.DB, error) { - db, err := sql.Open("sqlite3", dbPath) +func ConnectDB(dbURL string, token string) (*sql.DB, error) { + url := dbURL + "?authToken=" + token + + db, err := sql.Open("libsql", url) if err != nil { return nil, err } @@ -32,20 +35,24 @@ func InitDB(db *sql.DB) { } // loadAndConnectDB loads environment variables and connects to the database -func LoadAndConnectDB(dbPath string) *sql.DB { +func LoadAndConnectDB(dbURL string, token string) *sql.DB { log := logrus.New() - if dbPath == "" { + if dbURL == "" { log.Fatalf("Database path is not set") } - db, err := ConnectDB(dbPath) + if token == "" { + log.Fatalf("Token is not set") + } + + db, err := ConnectDB(dbURL, token) if err != nil { - log.Fatalf("Failed to connect to database %s: %v", dbPath, err) + log.Fatalf("Failed to connect to database %s: %v", dbURL, err) } log.WithFields(logrus.Fields{ - "dbPath": dbPath, + "dbPath": dbURL, }).Info("Connected to database") InitDB(db) diff --git a/main.go b/main.go index 295b2db..8ef9477 100644 --- a/main.go +++ b/main.go @@ -21,17 +21,17 @@ import ( // Config holds the application configuration type Config struct { - QuotesDBPath string - ImagesDBPath string - ServerPort string + DatabaseURL string + TursoAuthToken string + ServerPort string } // loadConfig loads configuration from environment variables func loadConfig() *Config { return &Config{ - QuotesDBPath: utils.GetEnv("QUOTES_DB_PATH", "default_quotes_db_path"), - ImagesDBPath: utils.GetEnv("IMAGES_DB_PATH", "default_images_db_path"), - ServerPort: utils.GetEnv("SERVER_PORT", "8080"), + DatabaseURL: utils.GetEnv("TURSO_DATABASE_URL", ""), + TursoAuthToken: utils.GetEnv("TURSO_AUTH_TOKEN", ""), + ServerPort: utils.GetEnv("SERVER_PORT", "8080"), } } @@ -45,16 +45,11 @@ func main() { config := loadConfig() // Connect to quotes_db - quotesDB := utils.LoadAndConnectDB(config.QuotesDBPath) - defer quotesDB.Close() - - // Connect to images_db - imagesDB := utils.LoadAndConnectDB(config.ImagesDBPath) - defer imagesDB.Close() - + db := utils.LoadAndConnectDB(config.DatabaseURL, config.TursoAuthToken) + defer db.Close() // Initialize handlers - quoteHandler := &handlers.QuoteHandler{DB: quotesDB} - imageHandler := &handlers.ImageHandler{DB: imagesDB} + quoteHandler := &handlers.QuoteHandler{DB: db} + imageHandler := &handlers.ImageHandler{DB: db} // Setup router and middleware r := chi.NewRouter() From d913f5101f023e9c5db2586717cda91fbe1383f4 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Fri, 30 Aug 2024 23:27:17 +0100 Subject: [PATCH 14/43] feat: environmental variables for quote table --- internal/handlers/image_handler.go | 3 ++- internal/handlers/quote_handler.go | 13 +++++++------ internal/models/quote.go | 26 +++++++++++++------------- main.go | 8 ++++++-- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index dbe086e..d8e6c89 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -11,7 +11,8 @@ import ( ) type ImageHandler struct { - DB *sql.DB + DB *sql.DB + TableName string } type Sizes struct { diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go index b78c8fc..002c56b 100644 --- a/internal/handlers/quote_handler.go +++ b/internal/handlers/quote_handler.go @@ -13,12 +13,13 @@ import ( ) type QuoteHandler struct { - DB *sql.DB + DB *sql.DB + TableName string } func (h *QuoteHandler) GetQuoteLanguages(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - languages, err := models.GetQuoteLanguages(ctx, h.DB) + languages, err := models.GetQuoteLanguages(ctx, h.DB, h.TableName) if err != nil { http.Error(w, "Failed to get quote languages", http.StatusInternalServerError) return @@ -31,7 +32,7 @@ func (h *QuoteHandler) GetQuoteLanguages(w http.ResponseWriter, r *http.Request) func (h *QuoteHandler) GetAllQuotes(w http.ResponseWriter, r *http.Request) { ctx := r.Context() language := r.URL.Query().Get("language") - quotes, err := models.GetAllQuotes(ctx, h.DB, language) + quotes, err := models.GetAllQuotes(ctx, h.DB, h.TableName, language) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -42,7 +43,7 @@ func (h *QuoteHandler) GetAllQuotes(w http.ResponseWriter, r *http.Request) { func (h *QuoteHandler) GetQuoteByID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id := chi.URLParam(r, "id") - quote, err := models.GetQuoteByID(ctx, h.DB, id) + quote, err := models.GetQuoteByID(ctx, h.DB, h.TableName, id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -75,12 +76,12 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { // Function to fetch a random quote with fallback to English fetchQuote := func(language string) (*models.Quote, error) { - quote, err := models.GetRandomQuoteExcluding(ctx, h.DB, language, seenQuotes) + quote, err := models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, language, seenQuotes) if err != nil && strings.Contains(err.Error(), "no quotes found") { // If no quotes are found, reset the seenQuotes list and try again log.Println("No quotes found, resetting seenQuotes list") seenQuotes = []string{} - quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, language, seenQuotes) + quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, language, seenQuotes) } return quote, err } diff --git a/internal/models/quote.go b/internal/models/quote.go index 53ee241..8fd302f 100644 --- a/internal/models/quote.go +++ b/internal/models/quote.go @@ -33,12 +33,12 @@ func init() { } // GetQuoteLanguages returns the available languages and the count of quotes for each language. -func GetQuoteLanguages(ctx context.Context, db *sql.DB) ([]LanguageCount, error) { - query := ` +func GetQuoteLanguages(ctx context.Context, db *sql.DB, tableName string) ([]LanguageCount, error) { + query := fmt.Sprintf(` SELECT language, COUNT(*) as count - FROM quotes + FROM %s GROUP BY language - ` + `, tableName) rows, err := db.QueryContext(ctx, query) if err != nil { @@ -66,12 +66,12 @@ func GetQuoteLanguages(ctx context.Context, db *sql.DB) ([]LanguageCount, error) } // GetAllQuotes fetches all quotes in the specified language. -func GetAllQuotes(ctx context.Context, db *sql.DB, language string) ([]Quote, error) { +func GetAllQuotes(ctx context.Context, db *sql.DB, tableName string, language string) ([]Quote, error) { if language == "" { language = "en" } - query := "SELECT id, quote, author, author_occupation FROM quotes WHERE language = ?" + query := fmt.Sprintf("SELECT id, quote, author, author_occupation FROM %s WHERE language = ?", tableName) rows, err := db.QueryContext(ctx, query, language) if err != nil { log.Printf("Error querying all quotes: %v", err) @@ -98,9 +98,9 @@ func GetAllQuotes(ctx context.Context, db *sql.DB, language string) ([]Quote, er } // GetQuoteByID fetches a quote by its ID in the specified language. -func GetQuoteByID(ctx context.Context, db *sql.DB, id string) (Quote, error) { +func GetQuoteByID(ctx context.Context, db *sql.DB, tableName string, id string) (Quote, error) { var quote Quote - query := "SELECT id, quote, author, author_occupation FROM quotes WHERE id = ?" + query := fmt.Sprintf("SELECT id, quote, author, author_occupation FROM %s WHERE id = ?", tableName) err := db.QueryRowContext(ctx, query, id).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -115,13 +115,13 @@ func GetQuoteByID(ctx context.Context, db *sql.DB, id string) (Quote, error) { } // GetRandomQuote fetches a random quote in the specified language. -func GetRandomQuote(ctx context.Context, db *sql.DB, language string) (Quote, error) { +func GetRandomQuote(ctx context.Context, db *sql.DB, tableName string, language string) (Quote, error) { if language == "" { language = "en" } var quote Quote - query := "SELECT id, quote, author, author_occupation FROM quotes WHERE language = ? ORDER BY RANDOM() LIMIT 1" + query := fmt.Sprintf("SELECT id, quote, author, author_occupation FROM %s WHERE language = ? ORDER BY RANDOM() LIMIT 1", tableName) err := db.QueryRowContext(ctx, query, language).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -135,7 +135,7 @@ func GetRandomQuote(ctx context.Context, db *sql.DB, language string) (Quote, er return quote, nil } -func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, language string, exclude []string) (*Quote, error) { +func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, tableName string, language string, exclude []string) (*Quote, error) { excludeClause := "" if len(exclude) > 0 { placeholders := strings.Repeat("?,", len(exclude)-1) + "?" @@ -144,11 +144,11 @@ func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, language string, e query := fmt.Sprintf(` SELECT id, quote, author, author_occupation - FROM quotes + FROM %s WHERE language = ? %s ORDER BY RANDOM() LIMIT 1 - `, excludeClause) + `, tableName, excludeClause) args := make([]interface{}, len(exclude)+1) args[0] = language diff --git a/main.go b/main.go index 8ef9477..7aec1f9 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,8 @@ type Config struct { DatabaseURL string TursoAuthToken string ServerPort string + QuotesTable string + ImagesTable string } // loadConfig loads configuration from environment variables @@ -32,6 +34,8 @@ func loadConfig() *Config { DatabaseURL: utils.GetEnv("TURSO_DATABASE_URL", ""), TursoAuthToken: utils.GetEnv("TURSO_AUTH_TOKEN", ""), ServerPort: utils.GetEnv("SERVER_PORT", "8080"), + QuotesTable: utils.GetEnv("QUOTES_TABLE", "quotes"), + ImagesTable: utils.GetEnv("IMAGES_TABLE", "images"), } } @@ -48,8 +52,8 @@ func main() { db := utils.LoadAndConnectDB(config.DatabaseURL, config.TursoAuthToken) defer db.Close() // Initialize handlers - quoteHandler := &handlers.QuoteHandler{DB: db} - imageHandler := &handlers.ImageHandler{DB: db} + quoteHandler := &handlers.QuoteHandler{DB: db, TableName: config.QuotesTable} + imageHandler := &handlers.ImageHandler{DB: db, TableName: config.ImagesTable} // Setup router and middleware r := chi.NewRouter() From df537192aa1b5d768bc1846d3cfa2e1fe5d0b7b6 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sat, 31 Aug 2024 11:40:20 +0100 Subject: [PATCH 15/43] feat: Update image handler to use table name parameter in database queries --- internal/handlers/image_handler.go | 8 ++++---- internal/models/image.go | 29 +++++++++++++++-------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index d8e6c89..df9fca2 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -22,7 +22,7 @@ type Sizes struct { func (h *ImageHandler) GetImagePhotographers(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - photographers, err := models.GetImagePhotographers(ctx, h.DB) + photographers, err := models.GetImagePhotographers(ctx, h.DB, h.TableName) if err != nil { http.Error(w, "Failed to get photographers", http.StatusInternalServerError) return @@ -35,7 +35,7 @@ func (h *ImageHandler) GetImagePhotographers(w http.ResponseWriter, r *http.Requ func (h *ImageHandler) GetImageByID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() id := chi.URLParam(r, "id") - image, err := models.GetImageByID(ctx, h.DB, id) + image, err := models.GetImageByID(ctx, h.DB, h.TableName, id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -47,7 +47,7 @@ func (h *ImageHandler) GetImages(w http.ResponseWriter, r *http.Request) { ctx := r.Context() photographer := r.URL.Query().Get("photographer") category := r.URL.Query().Get("category") - images, err := models.GetImages(ctx, h.DB, photographer, category) + images, err := models.GetImages(ctx, h.DB, h.TableName, photographer, category) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -57,7 +57,7 @@ func (h *ImageHandler) GetImages(w http.ResponseWriter, r *http.Request) { func (h *ImageHandler) GetImageCategories(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - categories, err := models.GetImageCategories(ctx, h.DB) + categories, err := models.GetImageCategories(ctx, h.DB, h.TableName) if err != nil { http.Error(w, "Failed to get categories", http.StatusInternalServerError) return diff --git a/internal/models/image.go b/internal/models/image.go index a3d273a..f8d413d 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "fmt" "log" ) @@ -31,12 +32,12 @@ type CategoryCount struct { Count int } -func GetImagePhotographers(ctx context.Context, db *sql.DB) ([]PhotographerCount, error) { - query := ` +func GetImagePhotographers(ctx context.Context, db *sql.DB, tableName string) ([]PhotographerCount, error) { + query := fmt.Sprintf(` SELECT photographer, COUNT(*) as count - FROM images + FROM %s GROUP BY photographer - ` + `, tableName) rows, err := db.QueryContext(ctx, query) if err != nil { @@ -63,12 +64,12 @@ func GetImagePhotographers(ctx context.Context, db *sql.DB) ([]PhotographerCount return photographers, nil } -func GetImageCategories(ctx context.Context, db *sql.DB) ([]CategoryCount, error) { - query := ` +func GetImageCategories(ctx context.Context, db *sql.DB, tableName string) ([]CategoryCount, error) { + query := fmt.Sprintf(` SELECT category, COUNT(*) as count - FROM images + FROM %s GROUP BY category -` +`, tableName) rows, err := db.QueryContext(ctx, query) if err != nil { @@ -95,9 +96,9 @@ func GetImageCategories(ctx context.Context, db *sql.DB) ([]CategoryCount, error return categories, nil } -func GetImageByID(ctx context.Context, db *sql.DB, id string) (Image, error) { +func GetImageByID(ctx context.Context, db *sql.DB, tableName, id string) (Image, error) { var image Image - query := "SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash FROM images WHERE id = ?" + query := fmt.Sprintf("SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash FROM %s WHERE id = ?", tableName) err := db.QueryRowContext(ctx, query, id).Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -111,11 +112,11 @@ func GetImageByID(ctx context.Context, db *sql.DB, id string) (Image, error) { return image, nil } -func GetImages(ctx context.Context, db *sql.DB, photographer, category string) ([]Image, error) { - query := ` +func GetImages(ctx context.Context, db *sql.DB, tableName, photographer, category string) ([]Image, error) { + query := fmt.Sprintf(` SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash - FROM images - ` + FROM %s + `, tableName) var args []interface{} var conditions []string From b1e1cfa38178bdc8a2e2be10f54e54318f3f3405 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sat, 31 Aug 2024 12:03:02 +0100 Subject: [PATCH 16/43] refactor: Update Config struct with validation tags --- main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/main.go b/main.go index 7aec1f9..cff1fb6 100644 --- a/main.go +++ b/main.go @@ -21,11 +21,11 @@ import ( // Config holds the application configuration type Config struct { - DatabaseURL string - TursoAuthToken string - ServerPort string - QuotesTable string - ImagesTable string + DatabaseURL string `validate:"required,url"` + TursoAuthToken string `validate:"required"` + ServerPort string `validate:"required,numeric"` + QuotesTable string `validate:"required"` + ImagesTable string `validate:"required"` } // loadConfig loads configuration from environment variables @@ -39,7 +39,7 @@ func loadConfig() *Config { } } -// main is the entry point of the application +// main is the entry point of the applicationx func main() { // Load environment variables from .env file err := godotenv.Load() From b2c84aabb1e020548402bbbd2ee38eedb2489b71 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sat, 31 Aug 2024 12:19:39 +0100 Subject: [PATCH 17/43] refactor(main): Split up router functions --- go.mod | 10 ++++- go.sum | 18 ++++++++ main.go | 125 +++++++++++++++++++++++++++++++++++++------------------- 3 files changed, 110 insertions(+), 43 deletions(-) diff --git a/go.mod b/go.mod index f6ba113..b6acc5c 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,17 @@ require ( require ( github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/coder/websocket v1.8.12 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 // indirect github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 // indirect + golang.org/x/crypto v0.19.0 // indirect golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect - golang.org/x/sys v0.1.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index 4694842..4adca16 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,22 @@ github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NA github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLvn7D+wXjH9g4Jsjo+VqmzTUpl/LX7vfr6VOfSWTdM= github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= @@ -23,13 +33,21 @@ github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 h1:JclfLfq github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 h1:iLjJLq2A5J6L9zrhyNn+fpmxFvtEpYB4XLMr0rX3epI= github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index cff1fb6..9626b34 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/go-playground/validator/v10" "github.com/joho/godotenv" ) @@ -29,95 +30,135 @@ type Config struct { } // loadConfig loads configuration from environment variables -func loadConfig() *Config { - return &Config{ +func loadConfig() (*Config, error) { + config := &Config{ DatabaseURL: utils.GetEnv("TURSO_DATABASE_URL", ""), TursoAuthToken: utils.GetEnv("TURSO_AUTH_TOKEN", ""), ServerPort: utils.GetEnv("SERVER_PORT", "8080"), QuotesTable: utils.GetEnv("QUOTES_TABLE", "quotes"), ImagesTable: utils.GetEnv("IMAGES_TABLE", "images"), } + + // Validate configuration + if err := validator.New().Struct(config); err != nil { + return nil, err + } + + return config, nil } -// main is the entry point of the applicationx +// main is the entry point of the application func main() { // Load environment variables from .env file err := godotenv.Load() checkErr(err, "Error loading .env file") // Load the configuration - config := loadConfig() + config, err := loadConfig() + checkErr(err, "Invalid configuration") - // Connect to quotes_db + // Connect to db db := utils.LoadAndConnectDB(config.DatabaseURL, config.TursoAuthToken) + //checkErr(err, "Failed to connect to database") defer db.Close() + // Initialize handlers quoteHandler := &handlers.QuoteHandler{DB: db, TableName: config.QuotesTable} imageHandler := &handlers.ImageHandler{DB: db, TableName: config.ImagesTable} // Setup router and middleware + r := setupRouter(quoteHandler, imageHandler) + + // Start the server with graceful shutdown + startServer(r, config.ServerPort) +} + +// setupRouter configures the router and sub-routers +func setupRouter(quoteHandler *handlers.QuoteHandler, imageHandler *handlers.ImageHandler) *chi.Mux { r := chi.NewRouter() + + // Global middleware r.Use(middleware.RequestID) r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(local_middleware.NoCookieMiddleware) - // Define routes + // Root and health check routes r.Get("/", handlers.RootHandler) r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("OK")) }) - // Quote routes - r.Route("/quotes", func(r chi.Router) { - r.Get("/", quoteHandler.GetAllQuotes) - r.Get("/random", quoteHandler.GetRandomQuote) - r.Get("/languages", quoteHandler.GetQuoteLanguages) - - r.Route("/{id}", func(r chi.Router) { - r.Get("/", quoteHandler.GetQuoteByID) - // r.Put("/", quoteHandler.UpdateQuote) - // r.Delete("/", quoteHandler.DeleteQuote) - }) + // Mount sub-routers + r.Mount("/quotes", quotesRouter(quoteHandler)) + r.Mount("/images", imagesRouter(imageHandler)) + r.Mount("/debug", debugRouter()) + + return r +} + +// quotesRouter creates a sub-router for quote-related routes +func quotesRouter(handler *handlers.QuoteHandler) chi.Router { + r := chi.NewRouter() + r.Get("/", handler.GetAllQuotes) + r.Get("/random", handler.GetRandomQuote) + r.Get("/languages", handler.GetQuoteLanguages) + r.Route("/{id}", func(r chi.Router) { + r.Get("/", handler.GetQuoteByID) + // Add more quote-specific routes here (e.g., PUT, DELETE) }) + return r +} - // Image routes - r.Route("/images", func(r chi.Router) { - r.Get("/", imageHandler.GetImages) - r.Get("/photographers", imageHandler.GetImagePhotographers) - //r.Get("/random", imageHandler.GetRandomImage) - r.Get("/sizes", imageHandler.GetImageSizes) - r.Get("/categories", imageHandler.GetImageCategories) - r.Route("/{id}", func(r chi.Router) { - r.Get("/", imageHandler.GetImageByID) - // r.Put("/", imageHandler.UpdateImage) - // r.Delete("/", imageHandler.DeleteImage) - }) +// imagesRouter creates a sub-router for image-related routes +func imagesRouter(handler *handlers.ImageHandler) chi.Router { + r := chi.NewRouter() + r.Get("/", handler.GetImages) + r.Get("/photographers", handler.GetImagePhotographers) + //r.Get("/random", handler.GetRandomImage) + r.Get("/sizes", handler.GetImageSizes) + r.Get("/categories", handler.GetImageCategories) + r.Route("/{id}", func(r chi.Router) { + r.Get("/", handler.GetImageByID) + // Add more image-specific routes here (e.g., PUT, DELETE) }) - // pprof routes - r.HandleFunc("/debug/pprof/", pprof.Index) - r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - r.HandleFunc("/debug/pprof/profile", pprof.Profile) - r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - r.HandleFunc("/debug/pprof/trace", pprof.Trace) - - // Set up the server + return r +} + +// debugRouter creates a sub-router for debugging routes like pprof +func debugRouter() chi.Router { + r := chi.NewRouter() + r.HandleFunc("/pprof/", pprof.Index) + r.HandleFunc("/pprof/cmdline", pprof.Cmdline) + r.HandleFunc("/pprof/profile", pprof.Profile) + r.HandleFunc("/pprof/symbol", pprof.Symbol) + r.HandleFunc("/pprof/trace", pprof.Trace) + return r +} + +// startServer initializes and starts the server with graceful shutdown +func startServer(handler http.Handler, port string) { srv := &http.Server{ - Addr: ":" + config.ServerPort, - Handler: r, + Addr: ":" + port, + Handler: handler, } - // Graceful shutdown + // Start server in a goroutine go func() { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Failed to start server: %v", err) } }() - log.Printf("Server started on :%s", config.ServerPort) + log.Printf("Server started on :%s", port) + + // Graceful shutdown on interrupt signal + waitForShutdown(srv) +} - // Wait for interrupt signal to gracefully shutdown the server +// waitForShutdown handles graceful shutdown +func waitForShutdown(srv *http.Server) { quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit From 6f9e6be092d7da863369812c76386c71dfa858ce Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 31 Aug 2024 20:15:47 +0100 Subject: [PATCH 18/43] feat(image): Added random image endpoint --- internal/handlers/image_handler.go | 51 ++++++++++++++++++++++++++++++ internal/models/image.go | 35 ++++++++++++++++++++ main.go | 2 +- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index df9fca2..49a77cd 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -3,7 +3,9 @@ package handlers import ( "database/sql" "encoding/json" + "log" "net/http" + "strings" "mue-api/internal/models" @@ -81,4 +83,53 @@ func (h *ImageHandler) GetImageSizes(w http.ResponseWriter, r *http.Request) { } } +func (h *ImageHandler) GetRandomImage(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + cookie, err := r.Cookie("image-seen") + var seenImages []string + if err == nil { + seenImages = strings.Split(cookie.Value, ",") + } else { + // If the cookie does not exist, start with an empty list + seenImages = []string{} + log.Println("No seen_images cookie found, starting with an empty list") + } + + fetchImage := func() (*models.Image, error) { + image, err := models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages) + if err != nil && strings.Contains(err.Error(), "no images found") { + // If no quotes are found, reset the seenQuotes list and try again + log.Println("No quotes found, resetting seenImages list") + seenImages = []string{} + image, err = models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages) + } + return image, err + } + + image, err := fetchImage() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + seenImages = append(seenImages, image.ID) + + seenImagesStr := strings.Join(seenImages, ",") + newCookie := &http.Cookie{ + Name: "seen_quotes", + Value: seenImagesStr, + Path: "/", + // Consider setting MaxAge or Expires if you want the cookie to persist across sessions + } + http.SetCookie(w, newCookie) + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(image); err != nil { + log.Printf("Error encoding response: %v", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + // End of code diff --git a/internal/models/image.go b/internal/models/image.go index f8d413d..2cd16f9 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log" + "strings" ) type Image struct { @@ -160,3 +161,37 @@ func GetImages(ctx context.Context, db *sql.DB, tableName, photographer, categor return images, nil } + +func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, exclude []string) (*Image, error) { + excludeClause := "" + + if len(exclude) > 0 { + placeholders := strings.Repeat("?,", len(exclude)-1) + "?" + excludeClause = fmt.Sprintf("AND id NOT IN (%s)", placeholders) + } + + query := fmt.Sprintf(` + SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash + FROM %s + WHERE id NOT in (%s) + ORDER BY RANDOM() + LIMIT 1 + `, tableName, excludeClause) + + args := make([]interface{}, len(exclude)+1) + for i, id := range exclude { + args[i] = id + } + + row := db.QueryRowContext(ctx, query, args...) + var image Image + if err := row.Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash); err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("No image found") + } + return nil, err + } + + return &image, nil + +} diff --git a/main.go b/main.go index 9626b34..69640b1 100644 --- a/main.go +++ b/main.go @@ -117,7 +117,7 @@ func imagesRouter(handler *handlers.ImageHandler) chi.Router { r := chi.NewRouter() r.Get("/", handler.GetImages) r.Get("/photographers", handler.GetImagePhotographers) - //r.Get("/random", handler.GetRandomImage) + r.Get("/random", handler.GetRandomImage) r.Get("/sizes", handler.GetImageSizes) r.Get("/categories", handler.GetImageCategories) r.Route("/{id}", func(r chi.Router) { From d8e614596d24af9c45e29b48f17ee39e805771d4 Mon Sep 17 00:00:00 2001 From: David Ralph Date: Sun, 1 Sep 2024 00:32:34 +0100 Subject: [PATCH 19/43] chore: update license Signed-off-by: David Ralph --- LICENSE | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 9349203..e832a9a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License -Copyright (c) 2019-2022 The Mue Authors +Copyright (c) 2024 Kaiso One Ltd +Copyright (c) 2019-2024 The Mue Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From d9960782e0e77eed75a3add4ccdbdb57edc4d7a4 Mon Sep 17 00:00:00 2001 From: Isaac Date: Sun, 1 Sep 2024 23:16:17 +0100 Subject: [PATCH 20/43] Make `.env` file optional --- main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 69640b1..c8cbcff 100644 --- a/main.go +++ b/main.go @@ -51,7 +51,9 @@ func loadConfig() (*Config, error) { func main() { // Load environment variables from .env file err := godotenv.Load() - checkErr(err, "Error loading .env file") + if err != nil { + log.Printf("%s: %v", "Error loading .env file", err) + } // Load the configuration config, err := loadConfig() From 92b3c69bd81bc58ab6340e61ee5f4782107cf1b7 Mon Sep 17 00:00:00 2001 From: Isaac Date: Sun, 1 Sep 2024 23:16:38 +0100 Subject: [PATCH 21/43] Docker --- .dockerignore | 32 ++++++++++++++++++++++++++++++++ Dockerfile | 15 +++++++++++++++ go.mod | 2 +- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f5ee1e0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# flyctl launch added from .gitignore +# Directories +**\.idea +**\.vscode +**\.history +**\.wrangler + +# Files +**\.env +**\.dev.vars + +# Binaries for programs and plugins +**\*.exe +**\*.exe~ +**\*.dll +**\*.so +**\*.dylib +# Test binary, built with `go test -c` +**\*.test +# Output of the go coverage tool, specifically when used with LiteIDE +**\*.out +**\*.db +**\*.zip +**\*.DS_Store + +# Dependency directories (remove the comment below to include it) +# vendor/ +# Go workspace file +**\go.work +**\go.work.sum + +fly.toml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..ea1c901 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.23 AS builder +WORKDIR /build +RUN apt-get update && apt-get install -y gcc +COPY go.mod go.sum ./ +RUN go mod download +COPY ./ ./ +RUN CGO_ENABLED=1 GOOS=linux go build -ldflags '-extldflags "-static"' -o ./api + +FROM golang:1.23 AS runner +COPY --from=builder /build/api /api +ENV SERVER_PORT=80 +EXPOSE 80 +CMD ["/api"] \ No newline at end of file diff --git a/go.mod b/go.mod index b6acc5c..b2d8287 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module mue-api -go 1.23.0 +go 1.23 require ( github.com/go-chi/chi/v5 v5.1.0 From bcef7b31150955c329ea1b3baf70c50ba0e807b1 Mon Sep 17 00:00:00 2001 From: Isaac Date: Sun, 1 Sep 2024 23:16:48 +0100 Subject: [PATCH 22/43] Fly --- .github/workflows/fly-deploy.yml | 18 ++++++++++++++++++ fly.toml | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .github/workflows/fly-deploy.yml create mode 100644 fly.toml diff --git a/.github/workflows/fly-deploy.yml b/.github/workflows/fly-deploy.yml new file mode 100644 index 0000000..b0c246e --- /dev/null +++ b/.github/workflows/fly-deploy.yml @@ -0,0 +1,18 @@ +# See https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/ + +name: Fly Deploy +on: + push: + branches: + - main +jobs: + deploy: + name: Deploy app + runs-on: ubuntu-latest + concurrency: deploy-group # optional: ensure only one action runs at a time + steps: + - uses: actions/checkout@v4 + - uses: superfly/flyctl-actions/setup-flyctl@master + - run: flyctl deploy --remote-only + env: + FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..ef9c98d --- /dev/null +++ b/fly.toml @@ -0,0 +1,21 @@ +# fly.toml app configuration file generated for mue-api on 2024-09-01T22:02:41+01:00 +# +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app = 'mue-api' +primary_region = 'lhr' + +[build] + build-target = "runner" + +[http_service] + internal_port = 80 + force_https = true + auto_stop_machines = 'stop' + auto_start_machines = true + min_machines_running = 0 + processes = ['app'] + +[[vm]] + size = 'shared-cpu-1x' From 713fa01a2de2698d254b394944b1f0a642195916 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Sun, 1 Sep 2024 23:27:09 +0100 Subject: [PATCH 23/43] feat: Add JSON tags to Image and Quote structs --- internal/models/image.go | 22 +++++++++++----------- internal/models/quote.go | 8 ++++---- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/models/image.go b/internal/models/image.go index 2cd16f9..52963aa 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -10,17 +10,17 @@ import ( ) type Image struct { - ID string - Camera *string - CreatedAt *string - LocationData *string - Photographer string - Category string - OriginalFileName string - Colour string - PUN int - Version int - BlurHash string + ID string `json:"id"` + Camera *string `json:"camera,omitempty"` + CreatedAt *string `json:"created_at,omitempty"` + LocationData *string `json:"location_data,omitempty"` + Photographer string `json:"photographer"` + Category string `json:"category"` + OriginalFileName string `json:"original_file_name"` + Colour string `json:"colour"` + PUN int `json:"pun"` + Version int `json:"version"` + BlurHash string `json:"blur_hash"` } type PhotographerCount struct { diff --git a/internal/models/quote.go b/internal/models/quote.go index 8fd302f..24262e8 100644 --- a/internal/models/quote.go +++ b/internal/models/quote.go @@ -13,10 +13,10 @@ import ( // Quote represents a quote with its details. type Quote struct { - ID string - Quote string - Author *string - Occupation *string + ID string `json:"id"` + Quote string `json:"quote"` + Author *string `json:"author,omitempty"` + Occupation *string `json:"occupation,omitempty"` } // LanguageCount represents a language and its quote count. From c0acef702fb9fa111d24196ec7b819931a9b712b Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 2 Sep 2024 02:54:46 +0100 Subject: [PATCH 24/43] Update fly config --- fly.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/fly.toml b/fly.toml index ef9c98d..55e6be4 100644 --- a/fly.toml +++ b/fly.toml @@ -1,10 +1,10 @@ -# fly.toml app configuration file generated for mue-api on 2024-09-01T22:02:41+01:00 +# fly.toml app configuration file generated for mue-api on 2024-09-02T02:12:33+01:00 # # See https://fly.io/docs/reference/configuration/ for information about how to use this file. # app = 'mue-api' -primary_region = 'lhr' +primary_region = 'cdg' [build] build-target = "runner" @@ -12,7 +12,7 @@ primary_region = 'lhr' [http_service] internal_port = 80 force_https = true - auto_stop_machines = 'stop' + auto_stop_machines = 'suspend' auto_start_machines = true min_machines_running = 0 processes = ['app'] From 3f4906ec3848af21c22a7c45514d5b16e736f066 Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 2 Sep 2024 04:36:04 +0100 Subject: [PATCH 25/43] Fly health checks --- fly.toml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fly.toml b/fly.toml index 55e6be4..d383a0d 100644 --- a/fly.toml +++ b/fly.toml @@ -19,3 +19,12 @@ primary_region = 'cdg' [[vm]] size = 'shared-cpu-1x' + +[[services.http_checks]] + interval = 10000 + grace_period = "5s" + method = "get" + path = "/" + protocol = "http" + timeout = 2000 + tls_skip_verify = false \ No newline at end of file From 4a7c88624a990c8ae04d99a740726a594b011d42 Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 2 Sep 2024 04:45:35 +0100 Subject: [PATCH 26/43] Revert "Fly health checks" This reverts commit 3f4906ec3848af21c22a7c45514d5b16e736f066. --- fly.toml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/fly.toml b/fly.toml index d383a0d..55e6be4 100644 --- a/fly.toml +++ b/fly.toml @@ -19,12 +19,3 @@ primary_region = 'cdg' [[vm]] size = 'shared-cpu-1x' - -[[services.http_checks]] - interval = 10000 - grace_period = "5s" - method = "get" - path = "/" - protocol = "http" - timeout = 2000 - tls_skip_verify = false \ No newline at end of file From 75e4a2253c777a6a4368d7bf0dc57dbfbd565a87 Mon Sep 17 00:00:00 2001 From: Jack Date: Mon, 2 Sep 2024 12:59:56 +0100 Subject: [PATCH 27/43] feat(cookie): Added cookie.go model with ability to create and view cookies --- internal/handlers/image_handler.go | 19 ++------ internal/handlers/quote_handler.go | 26 +++-------- internal/models/cookie.go | 70 ++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 internal/models/cookie.go diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index 49a77cd..0d57f2a 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -87,15 +87,7 @@ func (h *ImageHandler) GetRandomImage(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - cookie, err := r.Cookie("image-seen") - var seenImages []string - if err == nil { - seenImages = strings.Split(cookie.Value, ",") - } else { - // If the cookie does not exist, start with an empty list - seenImages = []string{} - log.Println("No seen_images cookie found, starting with an empty list") - } + var seenImages = models.GetCookieValueAsList(r, "seen_images") fetchImage := func() (*models.Image, error) { image, err := models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages) @@ -117,13 +109,8 @@ func (h *ImageHandler) GetRandomImage(w http.ResponseWriter, r *http.Request) { seenImages = append(seenImages, image.ID) seenImagesStr := strings.Join(seenImages, ",") - newCookie := &http.Cookie{ - Name: "seen_quotes", - Value: seenImagesStr, - Path: "/", - // Consider setting MaxAge or Expires if you want the cookie to persist across sessions - } - http.SetCookie(w, newCookie) + + models.SetCookie(w, "seen_images", seenImagesStr) w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(image); err != nil { diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go index 002c56b..bde9473 100644 --- a/internal/handlers/quote_handler.go +++ b/internal/handlers/quote_handler.go @@ -64,15 +64,7 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { } // Retrieve the list of quote IDs from the client's cookies - cookie, err := r.Cookie("seen_quotes") - var seenQuotes []string - if err == nil { - seenQuotes = strings.Split(cookie.Value, ",") - } else { - // If the cookie does not exist, start with an empty list - seenQuotes = []string{} - log.Println("No seen_quotes cookie found, starting with an empty list") - } + var seenQuotes = models.GetCookieValueAsList(r, "seen_quotes") // Function to fetch a random quote with fallback to English fetchQuote := func(language string) (*models.Quote, error) { @@ -97,21 +89,13 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { } } - // Add the new quote ID to the seenQuotes list if it’s not already there - if !contains(seenQuotes, quote.ID) { - seenQuotes = append(seenQuotes, quote.ID) - } + //Add the seen quote ID to the list of seen quotes + seenQuotes = append(seenQuotes, quote.ID) - // Update the cookie with the new list of seen quote IDs seenQuotesStr := strings.Join(seenQuotes, ",") - newCookie := &http.Cookie{ - Name: "seen_quotes", - Value: seenQuotesStr, - Path: "/", - // Consider setting MaxAge or Expires if you want the cookie to persist across sessions - } - http.SetCookie(w, newCookie) + // Update the cookie with the new list of seen quote IDs + models.SetCookie(w, "seen_quotes", seenQuotesStr) // Return the quote as JSON w.Header().Set("Content-Type", "application/json") diff --git a/internal/models/cookie.go b/internal/models/cookie.go new file mode 100644 index 0000000..cb679f4 --- /dev/null +++ b/internal/models/cookie.go @@ -0,0 +1,70 @@ +package models + +import ( + "log" + "net/http" + "strings" +) + +// Cookie Custom type to allow for defaults +type Cookie struct { + Name string + Value string + Path string `default:"/"` + MaxAge int `default:"0"` +} + +// Option allows for options when making cookies +type Option func(*Cookie) + +func SetCookie(w http.ResponseWriter, name string, value string, options ...Option) { + + //Creates a cookie with required and defaulted values + newCookie := &Cookie{ + Name: name, + Value: value, + } + + //Goes through the list of options inputted if any and changes the value + for _, option := range options { + option(newCookie) + } + + httpCookie := &http.Cookie{ + Name: newCookie.Name, + Value: newCookie.Value, + Path: newCookie.Path, + MaxAge: newCookie.MaxAge, + } + + http.SetCookie(w, httpCookie) +} + +//List of options that could be added +//Example use: models.SetCookie(w, "seen_quotes", seenQuotesStr, models.WithMaxAge(30)) + +func WithMaxAge(age int) Option { + return func(cookie *Cookie) { + cookie.MaxAge = age + } +} + +func WithPath(path string) Option { + return func(cookie *Cookie) { + cookie.Path = path + } +} + +func GetCookieValueAsList(r *http.Request, name string) []string { + cookie, err := r.Cookie(name) + + var returnString []string + if err == nil { + returnString = strings.Split(cookie.Value, ",") + } else { + // If the cookie does not exist, start with an empty list + returnString = []string{} + log.Println("No cookie found, starting with an empty list") + } + return returnString +} From 9844f4263db421c24ee28c31bf4c99f21969c0e1 Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 2 Sep 2024 13:07:23 +0100 Subject: [PATCH 28/43] Fix checks --- fly.toml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/fly.toml b/fly.toml index 55e6be4..9622d81 100644 --- a/fly.toml +++ b/fly.toml @@ -17,5 +17,12 @@ primary_region = 'cdg' min_machines_running = 0 processes = ['app'] +[[http_service.checks]] + grace_period = "5s" + interval = "30s" + method = "GET" + timeout = "5s" + path = "/" + [[vm]] - size = 'shared-cpu-1x' + size = 'shared-cpu-1x' \ No newline at end of file From acb56358d87b9dfd7fca20b46742576d405bf601 Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 2 Sep 2024 13:55:16 +0100 Subject: [PATCH 29/43] Docker dev --- .dockerignore | 3 +++ compose.yml | 17 +++++++++++++++++ dev.Dockerfile | 21 +++++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 compose.yml create mode 100644 dev.Dockerfile diff --git a/.dockerignore b/.dockerignore index f5ee1e0..f5c0804 100644 --- a/.dockerignore +++ b/.dockerignore @@ -30,3 +30,6 @@ **\go.work.sum fly.toml + +.git/ +.github/ \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..926a7c6 --- /dev/null +++ b/compose.yml @@ -0,0 +1,17 @@ +services: + dev: + build: + context: . + dockerfile: ./dev.Dockerfile + develop: + watch: + - action: sync + path: ./ + target: /app + ignore: [] + - action: sync+restart + path: .env + target: /app/.env + - action: rebuild + path: go.mod + env_file: .env \ No newline at end of file diff --git a/dev.Dockerfile b/dev.Dockerfile new file mode 100644 index 0000000..e9e4cd9 --- /dev/null +++ b/dev.Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.23 + +RUN apt-get update && apt-get install -y gcc + +RUN useradd -ms /bin/sh -u 1001 app +USER app +WORKDIR /app + +COPY --chown=app:app go.mod go.sum ./ +RUN go mod download + +COPY --chown=app ./ ./ + +ENV SERVER_PORT=8080 +ENV CGO_ENABLED=1 + +CMD go run -ldflags '-extldflags "-static"' . + +EXPOSE 8080 \ No newline at end of file From 0a24c2c76d09c35d13c2986d6f265c22eea00149 Mon Sep 17 00:00:00 2001 From: Isaac Date: Mon, 2 Sep 2024 13:55:23 +0100 Subject: [PATCH 30/43] Embedded replicas --- go.mod | 7 ++----- go.sum | 19 ++++++++----------- internal/utils/database.go | 33 +++++++++++++++++++++++++-------- main.go | 28 ++++++++++++++++------------ 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index b2d8287..4a20dcf 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23 require ( github.com/go-chi/chi/v5 v5.1.0 - github.com/google/uuid v1.6.0 + github.com/go-playground/validator/v10 v10.22.0 github.com/joho/godotenv v1.5.1 github.com/mattn/go-sqlite3 v1.14.22 github.com/sirupsen/logrus v1.9.3 @@ -12,17 +12,14 @@ require ( require ( github.com/antlr4-go/antlr/v4 v4.13.0 // indirect - github.com/coder/websocket v1.8.12 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 // indirect - github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 // indirect golang.org/x/crypto v0.19.0 // indirect - golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 4adca16..5345e09 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,20 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= -github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= -github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -24,30 +23,28 @@ github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 h1:JLv github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06/go.mod h1:FUkZ5OHjlGPjnM2UyGJz9TypXQFgYqw6AFNO1UiROTM= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 h1:JclfLfqxOsICgvSNhF5W3cY2v5tylOtYJdTZKgPM8Bg= github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= -github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 h1:iLjJLq2A5J6L9zrhyNn+fpmxFvtEpYB4XLMr0rX3epI= -github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/utils/database.go b/internal/utils/database.go index 3dca079..ed5c0a3 100644 --- a/internal/utils/database.go +++ b/internal/utils/database.go @@ -2,20 +2,37 @@ package utils import ( "database/sql" + "fmt" "log" + "os" + "time" - _ "github.com/mattn/go-sqlite3" "github.com/sirupsen/logrus" - _ "github.com/tursodatabase/libsql-client-go/libsql" + "github.com/tursodatabase/go-libsql" ) -func ConnectDB(dbURL string, token string) (*sql.DB, error) { - url := dbURL + "?authToken=" + token +func ConnectDB(localFile string, dbURL string, token string) (*sql.DB, error) { + primaryUrl := dbURL + "?authToken=" + token - db, err := sql.Open("libsql", url) + connector, err := libsql.NewEmbeddedReplicaConnector( + localFile, + primaryUrl, + libsql.WithAuthToken(token), + libsql.WithSyncInterval(time.Minute), + ) if err != nil { - return nil, err + fmt.Println("Error creating connector:", err) + os.Exit(1) } + defer connector.Close() + + db := sql.OpenDB(connector) + defer db.Close() + + // db, err := sql.Open("libsql", url) + // if err != nil { + // return nil, err + // } // Ping the database to ensure the connection is established if err := db.Ping(); err != nil { @@ -35,7 +52,7 @@ func InitDB(db *sql.DB) { } // loadAndConnectDB loads environment variables and connects to the database -func LoadAndConnectDB(dbURL string, token string) *sql.DB { +func LoadAndConnectDB(localFile string, dbURL string, token string) *sql.DB { log := logrus.New() if dbURL == "" { @@ -46,7 +63,7 @@ func LoadAndConnectDB(dbURL string, token string) *sql.DB { log.Fatalf("Token is not set") } - db, err := ConnectDB(dbURL, token) + db, err := ConnectDB(localFile, dbURL, token) if err != nil { log.Fatalf("Failed to connect to database %s: %v", dbURL, err) } diff --git a/main.go b/main.go index c8cbcff..a3f4e8c 100644 --- a/main.go +++ b/main.go @@ -22,21 +22,25 @@ import ( // Config holds the application configuration type Config struct { - DatabaseURL string `validate:"required,url"` - TursoAuthToken string `validate:"required"` - ServerPort string `validate:"required,numeric"` - QuotesTable string `validate:"required"` - ImagesTable string `validate:"required"` + LocalDatabase string `validate:"required"` + // DatabaseSyncInterval string `validate:"required,url"` + PrimaryDatabaseURL string `validate:"required,url"` + TursoAuthToken string `validate:"required"` + ServerPort string `validate:"required,numeric"` + QuotesTable string `validate:"required"` + ImagesTable string `validate:"required"` } // loadConfig loads configuration from environment variables func loadConfig() (*Config, error) { config := &Config{ - DatabaseURL: utils.GetEnv("TURSO_DATABASE_URL", ""), - TursoAuthToken: utils.GetEnv("TURSO_AUTH_TOKEN", ""), - ServerPort: utils.GetEnv("SERVER_PORT", "8080"), - QuotesTable: utils.GetEnv("QUOTES_TABLE", "quotes"), - ImagesTable: utils.GetEnv("IMAGES_TABLE", "images"), + LocalDatabase: utils.GetEnv("DATABASE_CACHE_FILE", "replica.db"), + // DatabaseSyncInterval: utils.GetEnv("DATABASE_SYNC_INTERVAL", ""), + PrimaryDatabaseURL: utils.GetEnv("TURSO_DATABASE_URL", ""), + TursoAuthToken: utils.GetEnv("TURSO_AUTH_TOKEN", ""), + ServerPort: utils.GetEnv("SERVER_PORT", "8080"), + QuotesTable: utils.GetEnv("QUOTES_TABLE", "quotes"), + ImagesTable: utils.GetEnv("IMAGES_TABLE", "images"), } // Validate configuration @@ -52,7 +56,7 @@ func main() { // Load environment variables from .env file err := godotenv.Load() if err != nil { - log.Printf("%s: %v", "Error loading .env file", err) + log.Println(err) } // Load the configuration @@ -60,7 +64,7 @@ func main() { checkErr(err, "Invalid configuration") // Connect to db - db := utils.LoadAndConnectDB(config.DatabaseURL, config.TursoAuthToken) + db := utils.LoadAndConnectDB(config.LocalDatabase, config.PrimaryDatabaseURL, config.TursoAuthToken) //checkErr(err, "Failed to connect to database") defer db.Close() From f8941c286a720e28513cabe6825d0ceeb4d4a3b6 Mon Sep 17 00:00:00 2001 From: David Ralph Date: Tue, 3 Sep 2024 11:29:17 +0100 Subject: [PATCH 31/43] feat(weather): implement handlers, start models etc (wip) --- internal/handlers/root_handler.go | 5 + internal/handlers/weather_handler.go | 45 ++++++++ internal/models/weather.go | 152 +++++++++++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 internal/handlers/weather_handler.go create mode 100644 internal/models/weather.go diff --git a/internal/handlers/root_handler.go b/internal/handlers/root_handler.go index e5668f7..7e60627 100644 --- a/internal/handlers/root_handler.go +++ b/internal/handlers/root_handler.go @@ -17,6 +17,7 @@ type ApiInfo struct { Description string `json:"description"` QuoteEndpoints []Endpoint `json:"quote_endpoints"` ImageEndpoints []Endpoint `json:"image_endpoints"` + WeatherEndpoints []Endpoint `json:"weather_endpoints"` DocumentationURL string `json:"documentation_url"` } @@ -40,6 +41,10 @@ func RootHandler(w http.ResponseWriter, r *http.Request) { {Method: "GET", Path: "/images/categories", Description: "Retrieve all image categories."}, {Method: "GET", Path: "/images/sizes", Description: "Retrieve all image sizes."}, }, + WeatherEndpoints: []Endpoint{ + {Method: "GET", Path: "/weather", Description: "Retrieve the weather for a specific location."}, + {Method: "GET", Path: "/weather/autolocation", Description: "Retrieve the city for the user's current location."}, + }, DocumentationURL: "https://docs.muetab.com/", } diff --git a/internal/handlers/weather_handler.go b/internal/handlers/weather_handler.go new file mode 100644 index 0000000..18e3476 --- /dev/null +++ b/internal/handlers/weather_handler.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "database/sql" + "encoding/json" + "log" + "net/http" + "strings" + + "mue-api/internal/models" + + "github.com/go-chi/chi/v5" +) + +type WeatherHandler struct { +} + +func (h *WeatherHandler) GetLocationWeather(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + lat := r.URL.Query().Get("lat") + lon := r.URL.Query().Get("lon") + + weather, err := models.GetLocationWeather(ctx, lat, lon) + if err != nil { + http.Error(w, "Failed to get weather data", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(weather) +} + +func (h *WeatherHandler) GetWeather(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + city := r.URL.Query().Get("city") + + weather, err := models.GetWeather(ctx, city) + if err != nil { + http.Error(w, "Failed to get weather data", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(weather) +} \ No newline at end of file diff --git a/internal/models/weather.go b/internal/models/weather.go new file mode 100644 index 0000000..0d5906c --- /dev/null +++ b/internal/models/weather.go @@ -0,0 +1,152 @@ +import models + +import ( + "context" + "net/http" + "errors" + "fmt" + "log" + "strings" + "time" +) + +type Weather struct { + Weather []struct { + Main string `json:"main"` + Description string `json:"description"` + Icon string `json:"icon"` + } `json:"weather"` + Main struct { + Temp float64 `json:"temp"` + TempMin float64 `json:"temp_min"` + TempMax float64 `json:"temp_max"` + FeelsLike float64 `json:"feels_like"` + Pressure int `json:"pressure"` + Humidity int `json:"humidity"` + } `json:"main"` + Visibility int `json:"visibility"` + Wind struct { + Speed float64 `json:"speed"` + Deg int `json:"deg"` + } `json:"wind"` + Clouds struct { + All int `json:"all"` + } `json:"clouds"` + ID int `json:"id"` +} + +type WeatherLocation struct { +} + +// get api token +var apiToken string = "c1b3b3b" + +// get weather data +func GetLocationWeather(ctx context.Context, lat float64, lon float64) (*Weather, error) { + // build url + url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s", lat, lon, apiToken) + + // create request + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + + if err != nil { + log.Printf("Error creating request: %v", err) + return + } + + // send request + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.Printf("Error sending request: %v", err) + return + } + + // check response + if resp.StatusCode != http.StatusOK { + log.Printf("Error response: %v", resp.Status) + return + } + + // read response + defer resp.Body.Close() + + // parse response + var weather Weather + + if err := json.NewDecoder(resp.Body).Decode(&weather); err != nil { + log.Printf("Error decoding response: %v", err) + return + } + + return &weather, nil +} + +func GetWeather(ctx context.Context, city string) (*Weather, error) { + // build url + url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s", city, apiToken) + + // create request + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + + if err != nil { + log.Printf("Error creating request: %v", err) + return + } + + // send request + client := &http.Client{} + + resp, err := client.Do(req) + + if err != nil { + log.Printf("Error sending request: %v", err) + return + } + + // check response + if resp.StatusCode != http.StatusOK { + log.Printf("Error response: %v", resp.Status) + return + } + + // read response + defer resp.Body.Close() + + // parse response + var weather Weather + + if err := json.NewDecoder(resp.Body).Decode(&weather); err != nil { + log.Printf("Error decoding response: %v", err) + return + } + + /* + weather: [{ + main: data.weather[0].main, + description: data.weather[0].description, + icon: data.weather[0].icon + }], + main: { + temp: data.main.temp, + temp_min: data.main.temp_min, + temp_max: data.main.temp_max, + temp_feels_like: data.main.feels_like, + pressure: data.main.pressure, + humidity: data.main.humidity + }, + visibility: data.visibility, + wind: { + speed: data.wind.speed, + deg: data.wind.deg + }, + clouds: { + all: data.clouds.all + }, + id: data.id + */ + + return &weather, nil +} \ No newline at end of file From 74e5ae6dea36b0052dc2b7c7fd7754fd0f67d50d Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Tue, 3 Sep 2024 18:39:15 +0100 Subject: [PATCH 32/43] fix: change import to pacakge --- internal/handlers/weather_handler.go | 7 +-- internal/models/weather.go | 68 ++++++++++++++-------------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/internal/handlers/weather_handler.go b/internal/handlers/weather_handler.go index 18e3476..1f05332 100644 --- a/internal/handlers/weather_handler.go +++ b/internal/handlers/weather_handler.go @@ -1,15 +1,10 @@ package handlers import ( - "database/sql" "encoding/json" - "log" "net/http" - "strings" "mue-api/internal/models" - - "github.com/go-chi/chi/v5" ) type WeatherHandler struct { @@ -42,4 +37,4 @@ func (h *WeatherHandler) GetWeather(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(weather) -} \ No newline at end of file +} diff --git a/internal/models/weather.go b/internal/models/weather.go index 0d5906c..9a75395 100644 --- a/internal/models/weather.go +++ b/internal/models/weather.go @@ -1,13 +1,11 @@ -import models +package models import ( "context" - "net/http" - "errors" + "encoding/json" "fmt" "log" - "strings" - "time" + "net/http" ) type Weather struct { @@ -17,12 +15,12 @@ type Weather struct { Icon string `json:"icon"` } `json:"weather"` Main struct { - Temp float64 `json:"temp"` - TempMin float64 `json:"temp_min"` - TempMax float64 `json:"temp_max"` - FeelsLike float64 `json:"feels_like"` - Pressure int `json:"pressure"` - Humidity int `json:"humidity"` + Temp float64 `json:"temp"` + TempMin float64 `json:"temp_min"` + TempMax float64 `json:"temp_max"` + FeelsLike float64 `json:"feels_like"` + Pressure int `json:"pressure"` + Humidity int `json:"humidity"` } `json:"main"` Visibility int `json:"visibility"` Wind struct { @@ -124,29 +122,29 @@ func GetWeather(ctx context.Context, city string) (*Weather, error) { } /* - weather: [{ - main: data.weather[0].main, - description: data.weather[0].description, - icon: data.weather[0].icon - }], - main: { - temp: data.main.temp, - temp_min: data.main.temp_min, - temp_max: data.main.temp_max, - temp_feels_like: data.main.feels_like, - pressure: data.main.pressure, - humidity: data.main.humidity - }, - visibility: data.visibility, - wind: { - speed: data.wind.speed, - deg: data.wind.deg - }, - clouds: { - all: data.clouds.all - }, - id: data.id - */ + weather: [{ + main: data.weather[0].main, + description: data.weather[0].description, + icon: data.weather[0].icon + }], + main: { + temp: data.main.temp, + temp_min: data.main.temp_min, + temp_max: data.main.temp_max, + temp_feels_like: data.main.feels_like, + pressure: data.main.pressure, + humidity: data.main.humidity + }, + visibility: data.visibility, + wind: { + speed: data.wind.speed, + deg: data.wind.deg + }, + clouds: { + all: data.clouds.all + }, + id: data.id + */ return &weather, nil -} \ No newline at end of file +} From 1cf0a5a36ae18b0cfedf6b62c3d1a50867871d97 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Tue, 3 Sep 2024 18:53:32 +0100 Subject: [PATCH 33/43] fix(weather): number of returned arguments errors --- internal/models/image.go | 2 +- internal/models/weather.go | 20 ++++++++++---------- main.go | 24 ++++++++++++------------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/internal/models/image.go b/internal/models/image.go index 52963aa..ad0bab1 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -187,7 +187,7 @@ func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, var image Image if err := row.Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash); err != nil { if err == sql.ErrNoRows { - return nil, fmt.Errorf("No image found") + return nil, fmt.Errorf("no image found") } return nil, err } diff --git a/internal/models/weather.go b/internal/models/weather.go index 9a75395..17e9888 100644 --- a/internal/models/weather.go +++ b/internal/models/weather.go @@ -40,16 +40,16 @@ type WeatherLocation struct { var apiToken string = "c1b3b3b" // get weather data -func GetLocationWeather(ctx context.Context, lat float64, lon float64) (*Weather, error) { +func GetLocationWeather(ctx context.Context, lat, lon string) (*Weather, error) { // build url - url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s", lat, lon, apiToken) + url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%s&lon=%s&appid=%s", lat, lon, apiToken) // create request req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { log.Printf("Error creating request: %v", err) - return + return nil, err } // send request @@ -59,13 +59,13 @@ func GetLocationWeather(ctx context.Context, lat float64, lon float64) (*Weather if err != nil { log.Printf("Error sending request: %v", err) - return + return nil, err } // check response if resp.StatusCode != http.StatusOK { log.Printf("Error response: %v", resp.Status) - return + return nil, err } // read response @@ -76,7 +76,7 @@ func GetLocationWeather(ctx context.Context, lat float64, lon float64) (*Weather if err := json.NewDecoder(resp.Body).Decode(&weather); err != nil { log.Printf("Error decoding response: %v", err) - return + return nil, err } return &weather, nil @@ -91,7 +91,7 @@ func GetWeather(ctx context.Context, city string) (*Weather, error) { if err != nil { log.Printf("Error creating request: %v", err) - return + return nil, err } // send request @@ -101,13 +101,13 @@ func GetWeather(ctx context.Context, city string) (*Weather, error) { if err != nil { log.Printf("Error sending request: %v", err) - return + return nil, err } // check response if resp.StatusCode != http.StatusOK { log.Printf("Error response: %v", resp.Status) - return + return nil, err } // read response @@ -118,7 +118,7 @@ func GetWeather(ctx context.Context, city string) (*Weather, error) { if err := json.NewDecoder(resp.Body).Decode(&weather); err != nil { log.Printf("Error decoding response: %v", err) - return + return nil, err } /* diff --git a/main.go b/main.go index a3f4e8c..e6cf0a2 100644 --- a/main.go +++ b/main.go @@ -22,25 +22,25 @@ import ( // Config holds the application configuration type Config struct { - LocalDatabase string `validate:"required"` + LocalDatabase string `validate:"required"` // DatabaseSyncInterval string `validate:"required,url"` - PrimaryDatabaseURL string `validate:"required,url"` - TursoAuthToken string `validate:"required"` - ServerPort string `validate:"required,numeric"` - QuotesTable string `validate:"required"` - ImagesTable string `validate:"required"` + PrimaryDatabaseURL string `validate:"required,url"` + TursoAuthToken string `validate:"required"` + ServerPort string `validate:"required,numeric"` + QuotesTable string `validate:"required"` + ImagesTable string `validate:"required"` } // loadConfig loads configuration from environment variables func loadConfig() (*Config, error) { config := &Config{ - LocalDatabase: utils.GetEnv("DATABASE_CACHE_FILE", "replica.db"), + LocalDatabase: utils.GetEnv("DATABASE_CACHE_FILE", "replica.db"), // DatabaseSyncInterval: utils.GetEnv("DATABASE_SYNC_INTERVAL", ""), - PrimaryDatabaseURL: utils.GetEnv("TURSO_DATABASE_URL", ""), - TursoAuthToken: utils.GetEnv("TURSO_AUTH_TOKEN", ""), - ServerPort: utils.GetEnv("SERVER_PORT", "8080"), - QuotesTable: utils.GetEnv("QUOTES_TABLE", "quotes"), - ImagesTable: utils.GetEnv("IMAGES_TABLE", "images"), + PrimaryDatabaseURL: utils.GetEnv("TURSO_DATABASE_URL", ""), + TursoAuthToken: utils.GetEnv("TURSO_AUTH_TOKEN", ""), + ServerPort: utils.GetEnv("SERVER_PORT", "8080"), + QuotesTable: utils.GetEnv("QUOTES_TABLE", "quotes"), + ImagesTable: utils.GetEnv("IMAGES_TABLE", "images"), } // Validate configuration From 02bc66f49a14b3d6f11f53b0cad46b20a65feee0 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 4 Sep 2024 00:39:37 +0100 Subject: [PATCH 34/43] revert(database): Don't use replicas --- go.mod | 4 +++- go.sum | 6 ++++++ internal/utils/database.go | 33 ++++++++------------------------- main.go | 2 +- 4 files changed, 18 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index 4a20dcf..d1326d8 100644 --- a/go.mod +++ b/go.mod @@ -12,14 +12,16 @@ require ( require ( github.com/antlr4-go/antlr/v4 v4.13.0 // indirect + github.com/coder/websocket v1.8.12 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/libsql/sqlite-antlr4-parser v0.0.0-20240327125255-dbf53b6cbf06 // indirect github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 // indirect + github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d // indirect golang.org/x/crypto v0.19.0 // indirect - golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 5345e09..386133f 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -33,10 +35,14 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77 h1:JclfLfqxOsICgvSNhF5W3cY2v5tylOtYJdTZKgPM8Bg= github.com/tursodatabase/go-libsql v0.0.0-20240819180805-a9b092b8bc77/go.mod h1:TjsB2miB8RW2Sse8sdxzVTdeGlx74GloD5zJYUC38d8= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:dOMI4+zEbDI37KGb0TI44GUAwxHF9cMsIoDTJ7UmgfU= +github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= +golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/utils/database.go b/internal/utils/database.go index ed5c0a3..3dca079 100644 --- a/internal/utils/database.go +++ b/internal/utils/database.go @@ -2,37 +2,20 @@ package utils import ( "database/sql" - "fmt" "log" - "os" - "time" + _ "github.com/mattn/go-sqlite3" "github.com/sirupsen/logrus" - "github.com/tursodatabase/go-libsql" + _ "github.com/tursodatabase/libsql-client-go/libsql" ) -func ConnectDB(localFile string, dbURL string, token string) (*sql.DB, error) { - primaryUrl := dbURL + "?authToken=" + token +func ConnectDB(dbURL string, token string) (*sql.DB, error) { + url := dbURL + "?authToken=" + token - connector, err := libsql.NewEmbeddedReplicaConnector( - localFile, - primaryUrl, - libsql.WithAuthToken(token), - libsql.WithSyncInterval(time.Minute), - ) + db, err := sql.Open("libsql", url) if err != nil { - fmt.Println("Error creating connector:", err) - os.Exit(1) + return nil, err } - defer connector.Close() - - db := sql.OpenDB(connector) - defer db.Close() - - // db, err := sql.Open("libsql", url) - // if err != nil { - // return nil, err - // } // Ping the database to ensure the connection is established if err := db.Ping(); err != nil { @@ -52,7 +35,7 @@ func InitDB(db *sql.DB) { } // loadAndConnectDB loads environment variables and connects to the database -func LoadAndConnectDB(localFile string, dbURL string, token string) *sql.DB { +func LoadAndConnectDB(dbURL string, token string) *sql.DB { log := logrus.New() if dbURL == "" { @@ -63,7 +46,7 @@ func LoadAndConnectDB(localFile string, dbURL string, token string) *sql.DB { log.Fatalf("Token is not set") } - db, err := ConnectDB(localFile, dbURL, token) + db, err := ConnectDB(dbURL, token) if err != nil { log.Fatalf("Failed to connect to database %s: %v", dbURL, err) } diff --git a/main.go b/main.go index e6cf0a2..5bc1540 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,7 @@ func main() { checkErr(err, "Invalid configuration") // Connect to db - db := utils.LoadAndConnectDB(config.LocalDatabase, config.PrimaryDatabaseURL, config.TursoAuthToken) + db := utils.LoadAndConnectDB(config.PrimaryDatabaseURL, config.TursoAuthToken) //checkErr(err, "Failed to connect to database") defer db.Close() From bbf0c18b6ee248af3ecae5173e4d0ac9d9fc2c92 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 4 Sep 2024 00:49:45 +0100 Subject: [PATCH 35/43] feat(weather): Add endpoints + relevant data to struct --- internal/handlers/weather_handler.go | 3 +++ main.go | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/internal/handlers/weather_handler.go b/internal/handlers/weather_handler.go index 1f05332..57276cd 100644 --- a/internal/handlers/weather_handler.go +++ b/internal/handlers/weather_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "database/sql" "encoding/json" "net/http" @@ -8,6 +9,8 @@ import ( ) type WeatherHandler struct { + DB *sql.DB + APIToken string } func (h *WeatherHandler) GetLocationWeather(w http.ResponseWriter, r *http.Request) { diff --git a/main.go b/main.go index 5bc1540..993dbc5 100644 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ type Config struct { ServerPort string `validate:"required,numeric"` QuotesTable string `validate:"required"` ImagesTable string `validate:"required"` + WeatherAPIToken string `validate:"required"` } // loadConfig loads configuration from environment variables @@ -41,6 +42,7 @@ func loadConfig() (*Config, error) { ServerPort: utils.GetEnv("SERVER_PORT", "8080"), QuotesTable: utils.GetEnv("QUOTES_TABLE", "quotes"), ImagesTable: utils.GetEnv("IMAGES_TABLE", "images"), + WeatherAPIToken: utils.GetEnv("WEATHER_API_TOKEN", ""), } // Validate configuration @@ -71,16 +73,17 @@ func main() { // Initialize handlers quoteHandler := &handlers.QuoteHandler{DB: db, TableName: config.QuotesTable} imageHandler := &handlers.ImageHandler{DB: db, TableName: config.ImagesTable} + weatherHandler := &handlers.WeatherHandler{DB: db, APIToken: config.WeatherAPIToken} // Setup router and middleware - r := setupRouter(quoteHandler, imageHandler) + r := setupRouter(quoteHandler, imageHandler, weatherHandler) // Start the server with graceful shutdown startServer(r, config.ServerPort) } // setupRouter configures the router and sub-routers -func setupRouter(quoteHandler *handlers.QuoteHandler, imageHandler *handlers.ImageHandler) *chi.Mux { +func setupRouter(quoteHandler *handlers.QuoteHandler, imageHandler *handlers.ImageHandler, weatherHandler *handlers.WeatherHandler) *chi.Mux { r := chi.NewRouter() // Global middleware @@ -100,6 +103,7 @@ func setupRouter(quoteHandler *handlers.QuoteHandler, imageHandler *handlers.Ima // Mount sub-routers r.Mount("/quotes", quotesRouter(quoteHandler)) r.Mount("/images", imagesRouter(imageHandler)) + r.Mount("/weather", weatherRouter(weatherHandler)) r.Mount("/debug", debugRouter()) return r @@ -133,6 +137,13 @@ func imagesRouter(handler *handlers.ImageHandler) chi.Router { return r } +func weatherRouter(handler *handlers.WeatherHandler) chi.Router { + r := chi.NewRouter() + r.Get("/weather", handler.GetWeather) + r.Get("/location", handler.GetLocationWeather) + return r +} + // debugRouter creates a sub-router for debugging routes like pprof func debugRouter() chi.Router { r := chi.NewRouter() From 8ff56d85d73dcc754a9052637d78ab3bdb0ae317 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 4 Sep 2024 01:07:33 +0100 Subject: [PATCH 36/43] chore: remove unused code --- internal/models/quote.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/internal/models/quote.go b/internal/models/quote.go index 24262e8..5d94dba 100644 --- a/internal/models/quote.go +++ b/internal/models/quote.go @@ -6,9 +6,7 @@ import ( "errors" "fmt" "log" - "math/rand" "strings" - "time" ) // Quote represents a quote with its details. @@ -25,13 +23,6 @@ type LanguageCount struct { Count int } -var rng *rand.Rand - -// Initialize the random generator once at package initialization. -func init() { - rng = rand.New(rand.NewSource(time.Now().UnixNano())) -} - // GetQuoteLanguages returns the available languages and the count of quotes for each language. func GetQuoteLanguages(ctx context.Context, db *sql.DB, tableName string) ([]LanguageCount, error) { query := fmt.Sprintf(` From 82483cff887028ba1cbbbe75835ab7af4e132c15 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Wed, 4 Sep 2024 22:35:49 +0100 Subject: [PATCH 37/43] feat: Add category filter to GetRandomImage and GetRandomQuote handlers Co-authored-by: Jack --- internal/handlers/image_handler.go | 19 ++++++--------- internal/handlers/quote_handler.go | 39 ++++++++---------------------- internal/models/image.go | 6 ++--- 3 files changed, 20 insertions(+), 44 deletions(-) diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index 0d57f2a..7913113 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -86,28 +86,23 @@ func (h *ImageHandler) GetImageSizes(w http.ResponseWriter, r *http.Request) { func (h *ImageHandler) GetRandomImage(w http.ResponseWriter, r *http.Request) { ctx := r.Context() + category := r.URL.Query().Get("category") var seenImages = models.GetCookieValueAsList(r, "seen_images") - fetchImage := func() (*models.Image, error) { - image, err := models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages) - if err != nil && strings.Contains(err.Error(), "no images found") { - // If no quotes are found, reset the seenQuotes list and try again - log.Println("No quotes found, resetting seenImages list") - seenImages = []string{} - image, err = models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages) - } - return image, err + image, err := models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, category) + if err != nil && strings.Contains(err.Error(), "no images found") { + // If no quotes are found, reset the seenQuotes list and try again + log.Println("No quotes found, resetting seenImages list") + seenImages = []string{} + image, err = models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, category) } - - image, err := fetchImage() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } seenImages = append(seenImages, image.ID) - seenImagesStr := strings.Join(seenImages, ",") models.SetCookie(w, "seen_images", seenImagesStr) diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go index bde9473..1f79d76 100644 --- a/internal/handlers/quote_handler.go +++ b/internal/handlers/quote_handler.go @@ -66,32 +66,23 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { // Retrieve the list of quote IDs from the client's cookies var seenQuotes = models.GetCookieValueAsList(r, "seen_quotes") - // Function to fetch a random quote with fallback to English - fetchQuote := func(language string) (*models.Quote, error) { - quote, err := models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, language, seenQuotes) - if err != nil && strings.Contains(err.Error(), "no quotes found") { - // If no quotes are found, reset the seenQuotes list and try again - log.Println("No quotes found, resetting seenQuotes list") - seenQuotes = []string{} - quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, language, seenQuotes) + // Fetch a random quote with fallback to reset seenList and default to english + quote, err := models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, language, seenQuotes) + if err != nil && strings.Contains(err.Error(), "no quotes found") { + log.Println("No quotes found, resetting seenQuotes list") + seenQuotes = []string{} + quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, language, seenQuotes) + if err != nil { + quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, "en", seenQuotes) } - return quote, err } - - // Try to fetch a quote in the requested language - quote, err := fetchQuote(language) if err != nil { - // If no quotes found, default to English - quote, err = fetchQuote("en") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + http.Error(w, err.Error(), http.StatusInternalServerError) + return } //Add the seen quote ID to the list of seen quotes seenQuotes = append(seenQuotes, quote.ID) - seenQuotesStr := strings.Join(seenQuotes, ",") // Update the cookie with the new list of seen quote IDs @@ -105,16 +96,6 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { } } -// Helper function to check if a slice contains a string -func contains(slice []string, item string) bool { - for _, s := range slice { - if s == item { - return true - } - } - return false -} - // parseAcceptLanguage simplifies the Accept-Language header to a basic language code func parseAcceptLanguage(header string) string { if header == "" { diff --git a/internal/models/image.go b/internal/models/image.go index ad0bab1..3eb8a79 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -162,7 +162,7 @@ func GetImages(ctx context.Context, db *sql.DB, tableName, photographer, categor return images, nil } -func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, exclude []string) (*Image, error) { +func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, exclude []string, category string) (*Image, error) { excludeClause := "" if len(exclude) > 0 { @@ -173,10 +173,10 @@ func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, query := fmt.Sprintf(` SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash FROM %s - WHERE id NOT in (%s) + WHERE id NOT in (%s) AND category = %s ORDER BY RANDOM() LIMIT 1 - `, tableName, excludeClause) + `, tableName, excludeClause, category) args := make([]interface{}, len(exclude)+1) for i, id := range exclude { From 50ef85ca77c791f140e3d7267b64c4ee33a4723b Mon Sep 17 00:00:00 2001 From: Isaac Date: Fri, 6 Sep 2024 23:18:00 +0100 Subject: [PATCH 38/43] fix(docker): dev dockerfile --- dev.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev.Dockerfile b/dev.Dockerfile index e9e4cd9..21ac3be 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -16,6 +16,6 @@ COPY --chown=app ./ ./ ENV SERVER_PORT=8080 ENV CGO_ENABLED=1 -CMD go run -ldflags '-extldflags "-static"' . +CMD go run . EXPOSE 8080 \ No newline at end of file From 17295d8a65e52ed42cddf93eed1be87760a2b366 Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 12 Sep 2024 14:41:13 +0100 Subject: [PATCH 39/43] feat(image,quote)!: Implemented Conditional Searches By using URL queries the API can now send a random quote/image based on user preference --- internal/handlers/image_handler.go | 11 ++++-- internal/handlers/quote_handler.go | 20 ++++++---- internal/models/image.go | 42 +++++++++++++-------- internal/models/quote.go | 60 ++++++++++++++---------------- internal/utils/database.go | 2 +- internal/utils/sql.go | 28 ++++++++++++++ 6 files changed, 103 insertions(+), 60 deletions(-) create mode 100644 internal/utils/sql.go diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index 7913113..6c17f35 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -84,18 +84,21 @@ func (h *ImageHandler) GetImageSizes(w http.ResponseWriter, r *http.Request) { } func (h *ImageHandler) GetRandomImage(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - category := r.URL.Query().Get("category") + queryParams := r.URL.Query() + + // Get categories from the query parameters + categories := queryParams["category"] + photographers := queryParams["photographer"] var seenImages = models.GetCookieValueAsList(r, "seen_images") - image, err := models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, category) + image, err := models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, categories, photographers) if err != nil && strings.Contains(err.Error(), "no images found") { // If no quotes are found, reset the seenQuotes list and try again log.Println("No quotes found, resetting seenImages list") seenImages = []string{} - image, err = models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, category) + image, err = models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, categories, photographers) } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go index 1f79d76..563d9fb 100644 --- a/internal/handlers/quote_handler.go +++ b/internal/handlers/quote_handler.go @@ -53,13 +53,19 @@ func (h *QuoteHandler) GetQuoteByID(w http.ResponseWriter, r *http.Request) { func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - language := r.URL.Query().Get("language") + defaultLanguage := []string{"en"} + queryParams := r.URL.Query() + + languages := queryParams["language"] + authors := queryParams["author"] // If no language query parameter is provided, use the Accept-Language header - if language == "" { - language = parseAcceptLanguage(r.Header.Get("Accept-Language")) + if len(languages) == 0 { + language := parseAcceptLanguage(r.Header.Get("Accept-Language")) if language == "" { - language = "en" // Default to English if no Accept-Language header is present + languages = append(languages, "en") // Default to English if no Accept-Language header is present + } else { + languages = append(languages, parseAcceptLanguage(r.Header.Get("Accept-Language"))) } } @@ -67,13 +73,13 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { var seenQuotes = models.GetCookieValueAsList(r, "seen_quotes") // Fetch a random quote with fallback to reset seenList and default to english - quote, err := models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, language, seenQuotes) + quote, err := models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, seenQuotes, languages, authors) if err != nil && strings.Contains(err.Error(), "no quotes found") { log.Println("No quotes found, resetting seenQuotes list") seenQuotes = []string{} - quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, language, seenQuotes) + quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, seenQuotes, languages, authors) if err != nil { - quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, "en", seenQuotes) + quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, seenQuotes, defaultLanguage, authors) } } if err != nil { diff --git a/internal/models/image.go b/internal/models/image.go index 3eb8a79..974d9a0 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -6,7 +6,7 @@ import ( "errors" "fmt" "log" - "strings" + "mue-api/internal/utils" ) type Image struct { @@ -162,26 +162,40 @@ func GetImages(ctx context.Context, db *sql.DB, tableName, photographer, categor return images, nil } -func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, exclude []string, category string) (*Image, error) { - excludeClause := "" +func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, excludeIDs []string, includeCategories []string, includePhotographers []string) (*Image, error) { + //Creates an interface which is filled with the excluded values and a placeholder for whereConditions + var args []interface{} + var whereConditions []string - if len(exclude) > 0 { - placeholders := strings.Repeat("?,", len(exclude)-1) + "?" - excludeClause = fmt.Sprintf("AND id NOT IN (%s)", placeholders) + //Builds the where clause dependent on what exclusions and inclusions apply + if len(excludeIDs) > 0 { + excludeClause := utils.BuildWhereClause("id NOT IN", excludeIDs, &args) + if excludeClause != "" { + whereConditions = append(whereConditions, excludeClause) + } + } + if len(includeCategories) > 0 { + categoryClause := utils.BuildWhereClause("category IN", includeCategories, &args) + if categoryClause != "" { + whereConditions = append(whereConditions, categoryClause) + } + } + if len(includePhotographers) > 0 { + photographerClause := utils.BuildWhereClause("photographer IN", includePhotographers, &args) + if photographerClause != "" { + whereConditions = append(whereConditions, photographerClause) + } } + whereClause := utils.CombineWhereClause(whereConditions) + query := fmt.Sprintf(` SELECT id, camera, created_at, location_data, photographer, category, original_file_name, colour, pun, version, blur_hash FROM %s - WHERE id NOT in (%s) AND category = %s + %s ORDER BY RANDOM() LIMIT 1 - `, tableName, excludeClause, category) - - args := make([]interface{}, len(exclude)+1) - for i, id := range exclude { - args[i] = id - } + `, tableName, whereClause) row := db.QueryRowContext(ctx, query, args...) var image Image @@ -191,7 +205,5 @@ func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, } return nil, err } - return &image, nil - } diff --git a/internal/models/quote.go b/internal/models/quote.go index 5d94dba..e14f92a 100644 --- a/internal/models/quote.go +++ b/internal/models/quote.go @@ -6,7 +6,7 @@ import ( "errors" "fmt" "log" - "strings" + "mue-api/internal/utils" ) // Quote represents a quote with its details. @@ -105,53 +105,47 @@ func GetQuoteByID(ctx context.Context, db *sql.DB, tableName string, id string) return quote, nil } -// GetRandomQuote fetches a random quote in the specified language. -func GetRandomQuote(ctx context.Context, db *sql.DB, tableName string, language string) (Quote, error) { - if language == "" { - language = "en" - } +func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, tableName string, excludeIDs []string, includeLanguages []string, includeAuthours []string) (*Quote, error) { + //Creates an interface which is filled with the excluded values and a placeholder for whereConditions - var quote Quote - query := fmt.Sprintf("SELECT id, quote, author, author_occupation FROM %s WHERE language = ? ORDER BY RANDOM() LIMIT 1", tableName) - err := db.QueryRowContext(ctx, query, language).Scan("e.ID, "e.Quote, "e.Author, "e.Occupation) - if err != nil { - if errors.Is(err, sql.ErrNoRows) { - log.Printf("No quotes found for language %s", language) - return quote, errors.New("no quotes found") + var args []interface{} + var whereConditions []string + + //Builds the where clause dependent on what exclusions and inclusions apply + if len(excludeIDs) > 0 { + excludeClause := utils.BuildWhereClause("id NOT IN", excludeIDs, &args) + if excludeClause != "" { + whereConditions = append(whereConditions, excludeClause) } - log.Printf("Error querying random quote: %v", err) - return quote, err } - - return quote, nil -} - -func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, tableName string, language string, exclude []string) (*Quote, error) { - excludeClause := "" - if len(exclude) > 0 { - placeholders := strings.Repeat("?,", len(exclude)-1) + "?" - excludeClause = fmt.Sprintf("AND id NOT IN (%s)", placeholders) + if len(includeLanguages) > 0 { + languagesClause := utils.BuildWhereClause("language IN", includeLanguages, &args) + if languagesClause != "" { + whereConditions = append(whereConditions, languagesClause) + } } + if len(includeAuthours) > 0 { + authoursClause := utils.BuildWhereClause("author IN", includeAuthours, &args) + if authoursClause != "" { + whereConditions = append(whereConditions, authoursClause) + } + } + + whereClause := utils.CombineWhereClause(whereConditions) query := fmt.Sprintf(` SELECT id, quote, author, author_occupation FROM %s - WHERE language = ? %s + %s ORDER BY RANDOM() LIMIT 1 - `, tableName, excludeClause) - - args := make([]interface{}, len(exclude)+1) - args[0] = language - for i, id := range exclude { - args[i+1] = id - } + `, tableName, whereClause) row := db.QueryRowContext(ctx, query, args...) var quote Quote if err := row.Scan("e.ID, "e.Quote, "e.Author, "e.Occupation); err != nil { if err == sql.ErrNoRows { - return nil, fmt.Errorf("no quotes found for language %s excluding provided IDs", language) + return nil, fmt.Errorf("no quotes found") } return nil, err } diff --git a/internal/utils/database.go b/internal/utils/database.go index 3dca079..556b512 100644 --- a/internal/utils/database.go +++ b/internal/utils/database.go @@ -34,7 +34,7 @@ func InitDB(db *sql.DB) { } } -// loadAndConnectDB loads environment variables and connects to the database +// LoadAndConnectDB loads environment variables and connects to the database func LoadAndConnectDB(dbURL string, token string) *sql.DB { log := logrus.New() diff --git a/internal/utils/sql.go b/internal/utils/sql.go new file mode 100644 index 0000000..7f02820 --- /dev/null +++ b/internal/utils/sql.go @@ -0,0 +1,28 @@ +package utils + +import ( + "fmt" + "strings" +) + +// BuildWhereClause creates a SQL where clause for requests, +// it takes the prefix (starting clause)then a list of items to iterate over and adds it to the list of args +func BuildWhereClause(prefix string, items []string, args *[]interface{}) string { + if len(items) == 0 { + return "" + } + placeholders := make([]string, len(items)) + for i, item := range items { + placeholders[i] = "?" + *args = append(*args, item) + } + return fmt.Sprintf("%s (%s)", prefix, strings.Join(placeholders, ",")) +} + +// CombineWhereClause combines a set of where clauses into one string +func CombineWhereClause(whereConditions []string) string { + if len(whereConditions) == 0 { + return "" + } + return "WHERE " + strings.Join(whereConditions, " AND ") +} From a65d695952fa7e6faf9327881fc058a94500d66f Mon Sep 17 00:00:00 2001 From: Jack Date: Thu, 12 Sep 2024 15:01:19 +0100 Subject: [PATCH 40/43] fix(image): Wrong error message --- internal/handlers/image_handler.go | 8 ++++---- internal/handlers/quote_handler.go | 2 +- internal/models/quote.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index 6c17f35..c64158f 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -94,13 +94,13 @@ func (h *ImageHandler) GetRandomImage(w http.ResponseWriter, r *http.Request) { var seenImages = models.GetCookieValueAsList(r, "seen_images") image, err := models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, categories, photographers) - if err != nil && strings.Contains(err.Error(), "no images found") { + if err != nil && strings.Contains(err.Error(), "no image found") { // If no quotes are found, reset the seenQuotes list and try again - log.Println("No quotes found, resetting seenImages list") + log.Println("No images found, resetting seenImages list") seenImages = []string{} + models.SetCookie(w, "seen_images", "") image, err = models.GetRandomImageExcluding(ctx, h.DB, h.TableName, seenImages, categories, photographers) - } - if err != nil { + } else if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go index 563d9fb..aee139e 100644 --- a/internal/handlers/quote_handler.go +++ b/internal/handlers/quote_handler.go @@ -74,7 +74,7 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { // Fetch a random quote with fallback to reset seenList and default to english quote, err := models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, seenQuotes, languages, authors) - if err != nil && strings.Contains(err.Error(), "no quotes found") { + if err != nil && strings.Contains(err.Error(), "no quote found") { log.Println("No quotes found, resetting seenQuotes list") seenQuotes = []string{} quote, err = models.GetRandomQuoteExcluding(ctx, h.DB, h.TableName, seenQuotes, languages, authors) diff --git a/internal/models/quote.go b/internal/models/quote.go index e14f92a..52b8e76 100644 --- a/internal/models/quote.go +++ b/internal/models/quote.go @@ -145,7 +145,7 @@ func GetRandomQuoteExcluding(ctx context.Context, db *sql.DB, tableName string, var quote Quote if err := row.Scan("e.ID, "e.Quote, "e.Author, "e.Occupation); err != nil { if err == sql.ErrNoRows { - return nil, fmt.Errorf("no quotes found") + return nil, fmt.Errorf("no quote found") } return nil, err } From 97e260dbc0da03b3fddd5a4434bfc6edab3a6413 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 13 Sep 2024 10:37:41 +0100 Subject: [PATCH 41/43] fix(image,quote): Syntax --- internal/handlers/image_handler.go | 4 ++-- internal/handlers/quote_handler.go | 2 +- internal/models/image.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/handlers/image_handler.go b/internal/handlers/image_handler.go index c64158f..1102ae7 100644 --- a/internal/handlers/image_handler.go +++ b/internal/handlers/image_handler.go @@ -88,8 +88,8 @@ func (h *ImageHandler) GetRandomImage(w http.ResponseWriter, r *http.Request) { queryParams := r.URL.Query() // Get categories from the query parameters - categories := queryParams["category"] - photographers := queryParams["photographer"] + categories := queryParams["categories"] + photographers := queryParams["photographers"] var seenImages = models.GetCookieValueAsList(r, "seen_images") diff --git a/internal/handlers/quote_handler.go b/internal/handlers/quote_handler.go index aee139e..4c82689 100644 --- a/internal/handlers/quote_handler.go +++ b/internal/handlers/quote_handler.go @@ -57,7 +57,7 @@ func (h *QuoteHandler) GetRandomQuote(w http.ResponseWriter, r *http.Request) { queryParams := r.URL.Query() languages := queryParams["language"] - authors := queryParams["author"] + authors := queryParams["quotes"] // If no language query parameter is provided, use the Accept-Language header if len(languages) == 0 { diff --git a/internal/models/image.go b/internal/models/image.go index 974d9a0..96f5394 100644 --- a/internal/models/image.go +++ b/internal/models/image.go @@ -200,7 +200,7 @@ func GetRandomImageExcluding(ctx context.Context, db *sql.DB, tableName string, row := db.QueryRowContext(ctx, query, args...) var image Image if err := row.Scan(&image.ID, &image.Camera, &image.CreatedAt, &image.LocationData, &image.Photographer, &image.Category, &image.OriginalFileName, &image.Colour, &image.PUN, &image.Version, &image.BlurHash); err != nil { - if err == sql.ErrNoRows { + if errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("no image found") } return nil, err From 91ad1dcbae53f6b044b0fa4264808ec632162156 Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Fri, 13 Sep 2024 11:38:00 +0100 Subject: [PATCH 42/43] refactor(readme): Update README.md with installation instructions and environment variables --- README.md | 60 ++++++++++++++++++-------- public/assets/mue-api-logo-banner.png | Bin 0 -> 209189 bytes 2 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 public/assets/mue-api-logo-banner.png diff --git a/README.md b/README.md index 87b8105..936dea5 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,46 @@ -# Mue API -Source code for the Mue API. This API is the default for random images and quotes in Mue. +

+ logo +

+ +## Table Of Contents + +1. What is this API? +2. Installation +3. Env Variables +4. License + +## What is this API? + +The Mue API is created to serve HTTP requests for photos, quotes, weather and marketplace data to users. + +This API is used by the **[Mue Tab](https://github.com/mue/mue)** browser extension. ## Installation -See the [documentation](https://docs.muetab.com/development#api). - -## Environment - -Non-secret variables are in `wrangler.toml`. -Secrets are deployed with Wrangler but a `.dev.vars` file can be used in development: - -```sh -IS_LOCAL_MODE=1 -BASELIME_API_KEY= -MAPBOX_TOKEN= -OPENWEATHER_TOKEN= -PEXELS_TOKEN= -SUPABASE_TOKEN= -SUPABASE_URL= -UNSPLASH_TOKEN= + +To install this project, you will need the following on your machine: + +![Go](https://img.shields.io/badge/Go-00ADD8?logo=Go&logoColor=white&style=for-the-badge) + +Then run the commands: + +```bash +go get + +go run . ``` +## Env Variables + +| Name | Description | Required | Default value | Limitations | +| :-------------------: | :-------------------------------------------: | :------: | :-----------: | :----------------------------------------------: | +| `DATABASE_CACHE_FILE` | Local Cache Database Replica | ✅ | `replica.db` | Must be a string, must end in .db | +| `TURSO_DATABASE_URL` | The primary external database URL | ✅ | ❌ | Cannot be empty | +| `TURSO_AUTH_TOKEN` | Authentication token required to use Turso DB | ✅ | ❌ | Cannot be empty | +| `SERVER_PORT` | Default Server Port API hosted on | ❌ | `8080` | If set, must be a number between `0` and `65535` | +| `QUOTES_TABLE` | Name of Quotes Table | ❌ | `quotes` | Must be a string | +| `IMAGES_TABLE` | Name of Images Table | ❌ | `images` | Must be a string | +| `WEATHER_API_TOKEN` | OpenWeather API Token | ✅ | ❌ | Can't be empty string | + ## License -[MIT](LICENSE) + +This project is licensed under the [MIT License](http://opensource.org/licenses/MIT). diff --git a/public/assets/mue-api-logo-banner.png b/public/assets/mue-api-logo-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..e16d0bfeb75738ea71841bb1896af36e0b649a5c GIT binary patch literal 209189 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%0DV!=+*OIXEf6%E#sCWBeTN%S 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�Fof12}5qs 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®(wiTj9=2Af=?$~D&N8!k@km5+ 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

90oN7f<4(ZX&4S#9 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 J7azMA^FJkG<9Gl7 literal 0 HcmV?d00001 From 79282308082af539e2b9cba96053321a3d79a410 Mon Sep 17 00:00:00 2001 From: Alex Sparkes Date: Fri, 13 Sep 2024 11:40:05 +0100 Subject: [PATCH 43/43] Update README.md Signed-off-by: Alex Sparkes --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 936dea5..b2cd1f7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ -

- logo -

+![mue-api-logo-banner](https://github.com/user-attachments/assets/e3747494-4034-41a2-b632-c4a7083b4585) ## Table Of Contents + 1. What is this API? 2. Installation 3. Env Variables