From c7ef165f119c0863447f932727e6a22322bd0301 Mon Sep 17 00:00:00 2001 From: ch-iv Date: Tue, 9 Dec 2025 23:03:00 -0500 Subject: [PATCH] Implement local testing with supabase --- .github/workflows/ci.yml | 3 + .vscode/extensions.json | 2 +- .vscode/settings.json | 24 +- package-lock.json | 336 +++++++++- package.json | 6 +- src/lib/client/supabase/server.ts | 4 +- supabase/.gitignore | 8 + supabase/config.toml | 382 +++++++++++ .../20251210030724_remote_schema.sql | 609 ++++++++++++++++++ tests/helpers/fixtures.ts | 58 ++ tests/helpers/index.ts | 2 + tests/helpers/supabase.ts | 67 ++ tests/lib/api/getExample.integration.test.ts | 45 ++ tests/setup.ts | 15 + vitest.config.ts | 1 + 15 files changed, 1533 insertions(+), 29 deletions(-) create mode 100644 supabase/.gitignore create mode 100644 supabase/config.toml create mode 100644 supabase/migrations/20251210030724_remote_schema.sql create mode 100644 tests/helpers/fixtures.ts create mode 100644 tests/helpers/index.ts create mode 100644 tests/helpers/supabase.ts create mode 100644 tests/lib/api/getExample.integration.test.ts create mode 100644 tests/setup.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9bd908..de579de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: - name: Install dependencies run: npm install + - name: Start the database + run: npm run db + - name: Run tests run: npm test diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1d7ac85..74baffc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] + "recommendations": ["denoland.vscode-deno"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 61a823e..0e7024a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,26 @@ { - "editor.formatOnSave": true, + "[typescript]": { + "editor.defaultFormatter": "denoland.vscode-deno" + }, + "deno.enablePaths": ["supabase/functions"], + "deno.lint": true, + "deno.unstable": [ + "bare-node-builtins", + "byonm", + "sloppy-imports", + "unsafe-proto", + "webgpu", + "broadcast-channel", + "worker-options", + "cron", + "kv", + "ffi", + "fs", + "http", + "net" + ], "editor.codeActionsOnSave": { "source.fixAll.eslint": "always" - } + }, + "editor.formatOnSave": true } diff --git a/package-lock.json b/package-lock.json index cad0e47..051eb58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@supabase/ssr": "^0.7.0", "@supabase/supabase-js": "^2.80.0", + "dotenv": "^17.2.3", "husky": "^9.1.7", "lint-staged": "^16.2.7", "next": "15.5.6", @@ -28,7 +29,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "prettier": "^3.6.2", - "supabase": "^2.54.11", + "supabase": "^2.65.7", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5", @@ -42,6 +43,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -1775,6 +1801,7 @@ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.80.0.tgz", "integrity": "sha512-n8pkXQxuo5zCWXX5cbSNZj1vuWS8IVNGWTmP1m31Iq1k0e8lPZ07PF08TRV79HHq3mEPP/Ko//BQuflHvY2o8w==", "license": "MIT", + "peer": true, "dependencies": { "@supabase/auth-js": "2.80.0", "@supabase/functions-js": "2.80.0", @@ -1794,6 +1821,36 @@ "tslib": "^2.8.0" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -1887,6 +1944,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1928,6 +1992,7 @@ "version": "20.19.22", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.22.tgz", "integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1943,6 +2008,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1952,6 +2018,7 @@ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "dev": true, + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -2008,6 +2075,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -2604,6 +2672,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2674,6 +2743,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -3328,6 +3407,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3359,6 +3448,25 @@ "node": ">=0.10.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3626,6 +3734,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3713,6 +3822,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -3808,6 +3918,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -5284,6 +5395,16 @@ "loose-envify": "cli.js" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5871,6 +5992,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -5894,6 +6016,41 @@ "node": ">=6.0.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, "node_modules/proc-log": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", @@ -5948,6 +6105,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5956,6 +6114,7 @@ "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6724,9 +6883,9 @@ } }, "node_modules/supabase": { - "version": "2.58.5", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.58.5.tgz", - "integrity": "sha512-mYZSkUIePTdmwlHd26Pff8wpmjfre8gcuWzrc5QqhZgZvCXugVzAQQhcjaQisw5kusbPQWNIjUwcHYEKmejhPw==", + "version": "2.65.7", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.65.7.tgz", + "integrity": "sha512-HnG+HzRBoIibrbbjXTEAreJv/sA5MRkAV2SehpM12oVxdO+olKMwAqfafT4lnCMZ26tqYRGx5aIBL3DnRHtrQQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6852,6 +7011,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "peer": true, "engines": { "node": ">=12" }, @@ -7047,6 +7207,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7134,6 +7295,7 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -7227,6 +7389,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -7629,6 +7792,23 @@ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true }, + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true + }, "@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -8513,6 +8693,7 @@ "version": "2.80.0", "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.80.0.tgz", "integrity": "sha512-n8pkXQxuo5zCWXX5cbSNZj1vuWS8IVNGWTmP1m31Iq1k0e8lPZ07PF08TRV79HHq3mEPP/Ko//BQuflHvY2o8w==", + "peer": true, "requires": { "@supabase/auth-js": "2.80.0", "@supabase/functions-js": "2.80.0", @@ -8529,6 +8710,33 @@ "tslib": "^2.8.0" } }, + "@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "dependencies": { + "aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "requires": { + "dequal": "^2.0.3" + } + } + } + }, "@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -8594,6 +8802,12 @@ "tslib": "^2.4.0" } }, + "@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true + }, "@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -8632,6 +8846,7 @@ "version": "20.19.22", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.22.tgz", "integrity": "sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==", + "peer": true, "requires": { "undici-types": "~6.21.0" } @@ -8646,6 +8861,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "dev": true, + "peer": true, "requires": { "csstype": "^3.0.2" } @@ -8654,7 +8870,9 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", - "dev": true + "dev": true, + "peer": true, + "requires": {} }, "@types/ws": { "version": "8.18.1", @@ -8694,6 +8912,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, + "peer": true, "requires": { "@typescript-eslint/scope-manager": "8.46.2", "@typescript-eslint/types": "8.46.2", @@ -8727,7 +8946,8 @@ "version": "8.46.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", - "dev": true + "dev": true, + "requires": {} }, "@typescript-eslint/type-utils": { "version": "8.46.2", @@ -9041,13 +9261,15 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true + "dev": true, + "requires": {} }, "acorn-walk": { "version": "8.3.4", @@ -9084,6 +9306,12 @@ "environment": "^1.0.0" } }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -9534,6 +9762,12 @@ "object-keys": "^1.1.1" } }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true + }, "detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -9555,6 +9789,17 @@ "esutils": "^2.0.2" } }, + "dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true + }, + "dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==" + }, "dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -9767,6 +10012,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, + "peer": true, "requires": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -9826,7 +10072,9 @@ "version": "10.1.8", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true + "dev": true, + "peer": true, + "requires": {} }, "eslint-import-resolver-node": { "version": "0.3.9", @@ -9890,6 +10138,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "peer": true, "requires": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -10032,7 +10281,8 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", - "dev": true + "dev": true, + "requires": {} }, "eslint-scope": { "version": "8.4.0", @@ -10892,6 +11142,12 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true + }, "magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -11245,7 +11501,8 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", - "dev": true + "dev": true, + "peer": true }, "prettier-linter-helpers": { "version": "1.0.0", @@ -11256,6 +11513,31 @@ "fast-diff": "^1.1.2" } }, + "pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + } + } + }, "proc-log": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.0.0.tgz", @@ -11288,12 +11570,14 @@ "react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", - "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==" + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "peer": true }, "react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "peer": true, "requires": { "scheduler": "^0.26.0" } @@ -11811,9 +12095,9 @@ } }, "supabase": { - "version": "2.58.5", - "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.58.5.tgz", - "integrity": "sha512-mYZSkUIePTdmwlHd26Pff8wpmjfre8gcuWzrc5QqhZgZvCXugVzAQQhcjaQisw5kusbPQWNIjUwcHYEKmejhPw==", + "version": "2.65.7", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.65.7.tgz", + "integrity": "sha512-HnG+HzRBoIibrbbjXTEAreJv/sA5MRkAV2SehpM12oVxdO+olKMwAqfafT4lnCMZ26tqYRGx5aIBL3DnRHtrQQ==", "dev": true, "requires": { "bin-links": "^6.0.0", @@ -11885,13 +12169,15 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true + "dev": true, + "requires": {} }, "picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true + "dev": true, + "peer": true } } }, @@ -11913,7 +12199,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true + "dev": true, + "requires": {} }, "ts-node": { "version": "10.9.2", @@ -12018,7 +12305,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true + "dev": true, + "peer": true }, "unbox-primitive": { "version": "1.1.0", @@ -12085,6 +12373,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, + "peer": true, "requires": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -12099,13 +12388,15 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true + "dev": true, + "requires": {} }, "picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true + "dev": true, + "peer": true }, "postcss": { "version": "8.5.6", @@ -12293,7 +12584,8 @@ "ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==" + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "requires": {} }, "yallist": { "version": "5.0.0", diff --git a/package.json b/package.json index de4a404..7f5ed65 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,13 @@ "prepare": "husky", "typecheck": "tsc --noEmit", "check": "npm run format:check && npm run lint && npm run typecheck", - "fix": "npm run format && npm run lint:fix" + "fix": "npm run format && npm run lint:fix", + "db": "npx supabase start && npx supabase migration up && rm -f .env && npx supabase status -o env >> .env" }, "dependencies": { "@supabase/ssr": "^0.7.0", "@supabase/supabase-js": "^2.80.0", + "dotenv": "^17.2.3", "husky": "^9.1.7", "lint-staged": "^16.2.7", "next": "15.5.6", @@ -38,7 +40,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.4", "prettier": "^3.6.2", - "supabase": "^2.54.11", + "supabase": "^2.65.7", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "typescript": "^5", diff --git a/src/lib/client/supabase/server.ts b/src/lib/client/supabase/server.ts index 4430189..6ef15b7 100644 --- a/src/lib/client/supabase/server.ts +++ b/src/lib/client/supabase/server.ts @@ -8,8 +8,8 @@ import type { SupabaseClient } from "@supabase/supabase-js"; export async function createClient(): Promise> { const cookieStore = await cookies(); return createServerClient( - process.env["NEXT_PUBLIC_SUPABASE_URL"]!, - process.env["NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY"]!, + process.env["API_URL"]!, + process.env["PUBLISHABLE_KEY"]!, { cookies: { getAll() { diff --git a/supabase/.gitignore b/supabase/.gitignore new file mode 100644 index 0000000..ad9264f --- /dev/null +++ b/supabase/.gitignore @@ -0,0 +1,8 @@ +# Supabase +.branches +.temp + +# dotenvx +.env.keys +.env.local +.env.*.local diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000..ee490fa --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,382 @@ +# For detailed configuration reference documentation, visit: +# https://supabase.com/docs/guides/local-development/cli/config +# A string used to distinguish different Supabase projects on the same host. Defaults to the +# working directory name when running `supabase init`. +project_id = "trcc" + +[api] +enabled = true +# Port to use for the API URL. +port = 54321 +# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API +# endpoints. `public` and `graphql_public` schemas are included by default. +schemas = ["public", "graphql_public"] +# Extra schemas to add to the search_path of every request. +extra_search_path = ["public", "extensions"] +# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size +# for accidental or malicious requests. +max_rows = 1000 + +[api.tls] +# Enable HTTPS endpoints locally using a self-signed certificate. +enabled = false +# Paths to self-signed certificate pair. +# cert_path = "../certs/my-cert.pem" +# key_path = "../certs/my-key.pem" + +[db] +# Port to use for the local database URL. +port = 54322 +# Port used by db diff command to initialize the shadow database. +shadow_port = 54320 +# The database major version to use. This has to be the same as your remote database's. Run `SHOW +# server_version;` on the remote database to check. +major_version = 17 + +[db.pooler] +enabled = false +# Port to use for the local connection pooler. +port = 54329 +# Specifies when a server connection can be reused by other clients. +# Configure one of the supported pooler modes: `transaction`, `session`. +pool_mode = "transaction" +# How many server connections to allow per user/database pair. +default_pool_size = 20 +# Maximum number of client connections allowed. +max_client_conn = 100 + +# [db.vault] +# secret_key = "env(SECRET_VALUE)" + +[db.migrations] +# If disabled, migrations will be skipped during a db push or reset. +enabled = true +# Specifies an ordered list of schema files that describe your database. +# Supports glob patterns relative to supabase directory: "./schemas/*.sql" +schema_paths = [] + +[db.seed] +# If enabled, seeds the database after migrations during a db reset. +enabled = true +# Specifies an ordered list of seed files to load during db reset. +# Supports glob patterns relative to supabase directory: "./seeds/*.sql" +sql_paths = ["./seed.sql"] + +[db.network_restrictions] +# Enable management of network restrictions. +enabled = false +# List of IPv4 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv4 connections. Set empty array to block all IPs. +allowed_cidrs = ["0.0.0.0/0"] +# List of IPv6 CIDR blocks allowed to connect to the database. +# Defaults to allow all IPv6 connections. Set empty array to block all IPs. +allowed_cidrs_v6 = ["::/0"] + +[realtime] +enabled = false +# Bind realtime via either IPv4 or IPv6. (default: IPv4) +# ip_version = "IPv6" +# The maximum length in bytes of HTTP request headers. (default: 4096) +# max_header_length = 4096 + +[studio] +enabled = false +# Port to use for Supabase Studio. +port = 54323 +# External URL of the API server that frontend connects to. +api_url = "http://127.0.0.1" +# OpenAI API Key to use for Supabase AI in the Supabase Studio. +openai_api_key = "env(OPENAI_API_KEY)" + +# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they +# are monitored, and you can view the emails that would have been sent from the web interface. +[inbucket] +enabled = false +# Port to use for the email testing server web interface. +port = 54324 +# Uncomment to expose additional ports for testing user applications that send emails. +# smtp_port = 54325 +# pop3_port = 54326 +# admin_email = "admin@email.com" +# sender_name = "Admin" + +[storage] +enabled = false +# The maximum file size allowed (e.g. "5MB", "500KB"). +file_size_limit = "50MiB" + +# Uncomment to configure local storage buckets +# [storage.buckets.images] +# public = false +# file_size_limit = "50MiB" +# allowed_mime_types = ["image/png", "image/jpeg"] +# objects_path = "./images" + +# Uncomment to allow connections via S3 compatible clients +# [storage.s3_protocol] +# enabled = true + +# Image transformation API is available to Supabase Pro plan. +# [storage.image_transformation] +# enabled = true + +# Store analytical data in S3 for running ETL jobs over Iceberg Catalog +# This feature is only available on the hosted platform. +[storage.analytics] +enabled = false +max_namespaces = 5 +max_tables = 10 +max_catalogs = 2 + +# Analytics Buckets is available to Supabase Pro plan. +# [storage.analytics.buckets.my-warehouse] + +# Store vector embeddings in S3 for large and durable datasets +# This feature is only available on the hosted platform. +[storage.vector] +enabled = false +max_buckets = 10 +max_indexes = 5 + +# Vector Buckets is available to Supabase Pro plan. +# [storage.vector.buckets.documents-openai] + +[auth] +enabled = true +# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used +# in emails. +site_url = "http://127.0.0.1:3000" +# A list of *exact* URLs that auth providers are permitted to redirect to post authentication. +additional_redirect_urls = ["https://127.0.0.1:3000"] +# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). +jwt_expiry = 3600 +# JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1). +# jwt_issuer = "" +# Path to JWT signing key. DO NOT commit your signing keys file to git. +# signing_keys_path = "./signing_keys.json" +# If disabled, the refresh token will never expire. +enable_refresh_token_rotation = true +# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. +# Requires enable_refresh_token_rotation = true. +refresh_token_reuse_interval = 10 +# Allow/disallow new user signups to your project. +enable_signup = true +# Allow/disallow anonymous sign-ins to your project. +enable_anonymous_sign_ins = false +# Allow/disallow testing manual linking of accounts +enable_manual_linking = false +# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. +minimum_password_length = 6 +# Passwords that do not meet the following requirements will be rejected as weak. Supported values +# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` +password_requirements = "" + +[auth.rate_limit] +# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. +email_sent = 2 +# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. +sms_sent = 30 +# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. +anonymous_users = 30 +# Number of sessions that can be refreshed in a 5 minute interval per IP address. +token_refresh = 150 +# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). +sign_in_sign_ups = 30 +# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. +token_verifications = 30 +# Number of Web3 logins that can be made in a 5 minute interval per IP address. +web3 = 30 + +# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. +# [auth.captcha] +# enabled = true +# provider = "hcaptcha" +# secret = "" + +[auth.email] +# Allow/disallow new user signups via email to your project. +enable_signup = true +# If enabled, a user will be required to confirm any email change on both the old, and new email +# addresses. If disabled, only the new email is required to confirm. +double_confirm_changes = true +# If enabled, users need to confirm their email address before signing in. +enable_confirmations = false +# If enabled, users will need to reauthenticate or have logged in recently to change their password. +secure_password_change = false +# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. +max_frequency = "1s" +# Number of characters used in the email OTP. +otp_length = 6 +# Number of seconds before the email OTP expires (defaults to 1 hour). +otp_expiry = 3600 + +# Use a production-ready SMTP server +# [auth.email.smtp] +# enabled = true +# host = "smtp.sendgrid.net" +# port = 587 +# user = "apikey" +# pass = "env(SENDGRID_API_KEY)" +# admin_email = "admin@email.com" +# sender_name = "Admin" + +# Uncomment to customize email template +# [auth.email.template.invite] +# subject = "You have been invited" +# content_path = "./supabase/templates/invite.html" + +# Uncomment to customize notification email template +# [auth.email.notification.password_changed] +# enabled = true +# subject = "Your password has been changed" +# content_path = "./templates/password_changed_notification.html" + +[auth.sms] +# Allow/disallow new user signups via SMS to your project. +enable_signup = false +# If enabled, users need to confirm their phone number before signing in. +enable_confirmations = false +# Template for sending OTP to users +template = "Your code is {{ .Code }}" +# Controls the minimum amount of time that must pass before sending another sms otp. +max_frequency = "5s" + +# Use pre-defined map of phone number to OTP for testing. +# [auth.sms.test_otp] +# 4152127777 = "123456" + +# Configure logged in session timeouts. +# [auth.sessions] +# Force log out after the specified duration. +# timebox = "24h" +# Force log out if the user has been inactive longer than the specified duration. +# inactivity_timeout = "8h" + +# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. +# [auth.hook.before_user_created] +# enabled = true +# uri = "pg-functions://postgres/auth/before-user-created-hook" + +# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. +# [auth.hook.custom_access_token] +# enabled = true +# uri = "pg-functions:////" + +# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. +[auth.sms.twilio] +enabled = false +account_sid = "" +message_service_sid = "" +# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: +auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" + +# Multi-factor-authentication is available to Supabase Pro plan. +[auth.mfa] +# Control how many MFA factors can be enrolled at once per user. +max_enrolled_factors = 10 + +# Control MFA via App Authenticator (TOTP) +[auth.mfa.totp] +enroll_enabled = false +verify_enabled = false + +# Configure MFA via Phone Messaging +[auth.mfa.phone] +enroll_enabled = false +verify_enabled = false +otp_length = 6 +template = "Your code is {{ .Code }}" +max_frequency = "5s" + +# Configure MFA via WebAuthn +# [auth.mfa.web_authn] +# enroll_enabled = true +# verify_enabled = true + +# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, +# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, +# `twitter`, `slack`, `spotify`, `workos`, `zoom`. +[auth.external.apple] +enabled = false +client_id = "" +# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: +secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" +# Overrides the default auth redirectUrl. +redirect_uri = "" +# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, +# or any other third-party OIDC providers. +url = "" +# If enabled, the nonce check will be skipped. Required for local sign in with Google auth. +skip_nonce_check = false +# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. +email_optional = false + +# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. +# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. +[auth.web3.solana] +enabled = false + +# Use Firebase Auth as a third-party provider alongside Supabase Auth. +[auth.third_party.firebase] +enabled = false +# project_id = "my-firebase-project" + +# Use Auth0 as a third-party provider alongside Supabase Auth. +[auth.third_party.auth0] +enabled = false +# tenant = "my-auth0-tenant" +# tenant_region = "us" + +# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. +[auth.third_party.aws_cognito] +enabled = false +# user_pool_id = "my-user-pool-id" +# user_pool_region = "us-east-1" + +# Use Clerk as a third-party provider alongside Supabase Auth. +[auth.third_party.clerk] +enabled = false +# Obtain from https://clerk.com/setup/supabase +# domain = "example.clerk.accounts.dev" + +# OAuth server configuration +[auth.oauth_server] +# Enable OAuth server functionality +enabled = false +# Path for OAuth consent flow UI +authorization_url_path = "/oauth/consent" +# Allow dynamic client registration +allow_dynamic_registration = false + +[edge_runtime] +enabled = false +# Supported request policies: `oneshot`, `per_worker`. +# `per_worker` (default) — enables hot reload during local development. +# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). +policy = "per_worker" +# Port to attach the Chrome inspector for debugging edge functions. +inspector_port = 8083 +# The Deno major version to use. +deno_version = 2 + +# [edge_runtime.secrets] +# secret_key = "env(SECRET_VALUE)" + +[analytics] +enabled = true +port = 54327 +# Configure one of the supported backends: `postgres`, `bigquery`. +backend = "postgres" + +# Experimental features may be deprecated any time +[experimental] +# Configures Postgres storage engine to use OrioleDB (S3) +orioledb_version = "" +# Configures S3 bucket URL, eg. .s3-.amazonaws.com +s3_host = "env(S3_HOST)" +# Configures S3 bucket region, eg. us-east-1 +s3_region = "env(S3_REGION)" +# Configures AWS_ACCESS_KEY_ID for S3 bucket +s3_access_key = "env(S3_ACCESS_KEY)" +# Configures AWS_SECRET_ACCESS_KEY for S3 bucket +s3_secret_key = "env(S3_SECRET_KEY)" diff --git a/supabase/migrations/20251210030724_remote_schema.sql b/supabase/migrations/20251210030724_remote_schema.sql new file mode 100644 index 0000000..6870cc9 --- /dev/null +++ b/supabase/migrations/20251210030724_remote_schema.sql @@ -0,0 +1,609 @@ + + + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + + +COMMENT ON SCHEMA "public" IS 'standard public schema'; + + + +CREATE EXTENSION IF NOT EXISTS "pg_graphql" WITH SCHEMA "graphql"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pg_stat_statements" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA "extensions"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "supabase_vault" WITH SCHEMA "vault"; + + + + + + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA "extensions"; + + + + + +SET default_tablespace = ''; + +SET default_table_access_method = "heap"; + + +CREATE TABLE IF NOT EXISTS "public"."Cohorts" ( + "year" smallint NOT NULL, + "term" character varying NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "id" bigint NOT NULL, + CONSTRAINT "cohorts_term_check" CHECK ((("term")::"text" = ANY ((ARRAY['Fall'::character varying, 'Spring'::character varying, 'Summer'::character varying, 'Winter'::character varying])::"text"[]))) +); + + +ALTER TABLE "public"."Cohorts" OWNER TO "postgres"; + + +COMMENT ON TABLE "public"."Cohorts" IS 'e.g. Fall 2025, Spring 2023, etc.'; + + + +ALTER TABLE "public"."Cohorts" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME "public"."Cohorts_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + +CREATE TABLE IF NOT EXISTS "public"."Roles" ( + "name" character varying NOT NULL, + "type" character varying NOT NULL, + "is_active" boolean DEFAULT true NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "id" bigint NOT NULL, + CONSTRAINT "roles_type_check" CHECK ((("type")::"text" = ANY ((ARRAY['prior'::character varying, 'current'::character varying, 'future_interest'::character varying])::"text"[]))) +); + + +ALTER TABLE "public"."Roles" OWNER TO "postgres"; + + +ALTER TABLE "public"."Roles" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME "public"."Roles_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + +CREATE TABLE IF NOT EXISTS "public"."Users" ( + "id" "uuid" DEFAULT "auth"."uid"() NOT NULL, + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "email" character varying, + "role" character varying +); + + +ALTER TABLE "public"."Users" OWNER TO "postgres"; + + +COMMENT ON TABLE "public"."Users" IS 'Users of app (can be either user, staff, or admin)'; + + + +CREATE TABLE IF NOT EXISTS "public"."VolunteerCohorts" ( + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "cohort_id" bigint NOT NULL, + "volunteer_id" bigint NOT NULL +); + + +ALTER TABLE "public"."VolunteerCohorts" OWNER TO "postgres"; + + +COMMENT ON TABLE "public"."VolunteerCohorts" IS 'Relation between Volunteers and Cohorts'; + + + +CREATE TABLE IF NOT EXISTS "public"."VolunteerRoles" ( + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "role_id" bigint NOT NULL, + "volunteer_id" bigint NOT NULL +); + + +ALTER TABLE "public"."VolunteerRoles" OWNER TO "postgres"; + + +COMMENT ON TABLE "public"."VolunteerRoles" IS 'Relation between Volunteers and Roles'; + + + +CREATE TABLE IF NOT EXISTS "public"."Volunteers" ( + "name_org" character varying NOT NULL, + "pseudonym" character varying, + "pronouns" character varying, + "email" character varying, + "phone" character varying, + "position" character varying, + "opt_in_communication" boolean DEFAULT true, + "notes" "text", + "created_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "updated_at" timestamp with time zone DEFAULT "now"() NOT NULL, + "id" bigint NOT NULL, + CONSTRAINT "volunteers_position_check" CHECK ((("position")::"text" = ANY ((ARRAY['member'::character varying, 'volunteer'::character varying, 'staff'::character varying])::"text"[]))) +); + + +ALTER TABLE "public"."Volunteers" OWNER TO "postgres"; + + +ALTER TABLE "public"."Volunteers" ALTER COLUMN "id" ADD GENERATED BY DEFAULT AS IDENTITY ( + SEQUENCE NAME "public"."Volunteers_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1 +); + + + +ALTER TABLE ONLY "public"."Cohorts" + ADD CONSTRAINT "Cohorts_id_key" UNIQUE ("id"); + + + +ALTER TABLE ONLY "public"."Cohorts" + ADD CONSTRAINT "Cohorts_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."Roles" + ADD CONSTRAINT "Roles_id_key" UNIQUE ("id"); + + + +ALTER TABLE ONLY "public"."Roles" + ADD CONSTRAINT "Roles_name_key" UNIQUE ("name"); + + + +ALTER TABLE ONLY "public"."Roles" + ADD CONSTRAINT "Roles_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."Users" + ADD CONSTRAINT "Users_email_key" UNIQUE ("email"); + + + +ALTER TABLE ONLY "public"."VolunteerCohorts" + ADD CONSTRAINT "VolunteerCohorts_pkey" PRIMARY KEY ("cohort_id", "volunteer_id"); + + + +ALTER TABLE ONLY "public"."VolunteerRoles" + ADD CONSTRAINT "VolunteerRoles_pkey" PRIMARY KEY ("role_id", "volunteer_id"); + + + +ALTER TABLE ONLY "public"."Volunteers" + ADD CONSTRAINT "Volunteers_id_key" UNIQUE ("id"); + + + +ALTER TABLE ONLY "public"."Volunteers" + ADD CONSTRAINT "Volunteers_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."Users" + ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id"); + + + +ALTER TABLE ONLY "public"."VolunteerCohorts" + ADD CONSTRAINT "VolunteerCohorts_cohort_id_fkey" FOREIGN KEY ("cohort_id") REFERENCES "public"."Cohorts"("id") ON UPDATE CASCADE ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."VolunteerCohorts" + ADD CONSTRAINT "VolunteerCohorts_volunteer_id_fkey" FOREIGN KEY ("volunteer_id") REFERENCES "public"."Volunteers"("id") ON UPDATE CASCADE ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."VolunteerRoles" + ADD CONSTRAINT "VolunteerRoles_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "public"."Roles"("id") ON UPDATE CASCADE ON DELETE CASCADE; + + + +ALTER TABLE ONLY "public"."VolunteerRoles" + ADD CONSTRAINT "VolunteerRoles_volunteer_id_fkey" FOREIGN KEY ("volunteer_id") REFERENCES "public"."Volunteers"("id") ON UPDATE CASCADE ON DELETE CASCADE; + + + +ALTER TABLE "public"."Cohorts" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."Roles" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."Users" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."VolunteerCohorts" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."VolunteerRoles" ENABLE ROW LEVEL SECURITY; + + +ALTER TABLE "public"."Volunteers" ENABLE ROW LEVEL SECURITY; + + +CREATE POLICY "full permission for development phase" ON "public"."Cohorts" TO "anon" USING (true) WITH CHECK (true); + + + +CREATE POLICY "full permission for development phase" ON "public"."Roles" TO "anon" USING (true) WITH CHECK (true); + + + +CREATE POLICY "full permission for development phase" ON "public"."VolunteerCohorts" TO "anon" USING (true) WITH CHECK (true); + + + +CREATE POLICY "full permission for development phase" ON "public"."VolunteerRoles" TO "anon" USING (true) WITH CHECK (true); + + + +CREATE POLICY "full permissions for development phase" ON "public"."Volunteers" TO "anon" USING (true) WITH CHECK (true); + + + + + +ALTER PUBLICATION "supabase_realtime" OWNER TO "postgres"; + + +GRANT USAGE ON SCHEMA "public" TO "postgres"; +GRANT USAGE ON SCHEMA "public" TO "anon"; +GRANT USAGE ON SCHEMA "public" TO "authenticated"; +GRANT USAGE ON SCHEMA "public" TO "service_role"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +GRANT ALL ON TABLE "public"."Cohorts" TO "anon"; +GRANT ALL ON TABLE "public"."Cohorts" TO "authenticated"; +GRANT ALL ON TABLE "public"."Cohorts" TO "service_role"; + + + +GRANT ALL ON SEQUENCE "public"."Cohorts_id_seq" TO "anon"; +GRANT ALL ON SEQUENCE "public"."Cohorts_id_seq" TO "authenticated"; +GRANT ALL ON SEQUENCE "public"."Cohorts_id_seq" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."Roles" TO "anon"; +GRANT ALL ON TABLE "public"."Roles" TO "authenticated"; +GRANT ALL ON TABLE "public"."Roles" TO "service_role"; + + + +GRANT ALL ON SEQUENCE "public"."Roles_id_seq" TO "anon"; +GRANT ALL ON SEQUENCE "public"."Roles_id_seq" TO "authenticated"; +GRANT ALL ON SEQUENCE "public"."Roles_id_seq" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."Users" TO "anon"; +GRANT ALL ON TABLE "public"."Users" TO "authenticated"; +GRANT ALL ON TABLE "public"."Users" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."VolunteerCohorts" TO "anon"; +GRANT ALL ON TABLE "public"."VolunteerCohorts" TO "authenticated"; +GRANT ALL ON TABLE "public"."VolunteerCohorts" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."VolunteerRoles" TO "anon"; +GRANT ALL ON TABLE "public"."VolunteerRoles" TO "authenticated"; +GRANT ALL ON TABLE "public"."VolunteerRoles" TO "service_role"; + + + +GRANT ALL ON TABLE "public"."Volunteers" TO "anon"; +GRANT ALL ON TABLE "public"."Volunteers" TO "authenticated"; +GRANT ALL ON TABLE "public"."Volunteers" TO "service_role"; + + + +GRANT ALL ON SEQUENCE "public"."Volunteers_id_seq" TO "anon"; +GRANT ALL ON SEQUENCE "public"."Volunteers_id_seq" TO "authenticated"; +GRANT ALL ON SEQUENCE "public"."Volunteers_id_seq" TO "service_role"; + + + + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role"; + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role"; + + + + + + +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "anon"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated"; +ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role"; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +drop extension if exists "pg_net"; + +alter table "public"."Cohorts" drop constraint "cohorts_term_check"; + +alter table "public"."Roles" drop constraint "roles_type_check"; + +alter table "public"."Volunteers" drop constraint "volunteers_position_check"; + +alter table "public"."Cohorts" add constraint "cohorts_term_check" CHECK (((term)::text = ANY ((ARRAY['Fall'::character varying, 'Spring'::character varying, 'Summer'::character varying, 'Winter'::character varying])::text[]))) not valid; + +alter table "public"."Cohorts" validate constraint "cohorts_term_check"; + +alter table "public"."Roles" add constraint "roles_type_check" CHECK (((type)::text = ANY ((ARRAY['prior'::character varying, 'current'::character varying, 'future_interest'::character varying])::text[]))) not valid; + +alter table "public"."Roles" validate constraint "roles_type_check"; + +alter table "public"."Volunteers" add constraint "volunteers_position_check" CHECK ((("position")::text = ANY ((ARRAY['member'::character varying, 'volunteer'::character varying, 'staff'::character varying])::text[]))) not valid; + +alter table "public"."Volunteers" validate constraint "volunteers_position_check"; + + diff --git a/tests/helpers/fixtures.ts b/tests/helpers/fixtures.ts new file mode 100644 index 0000000..0a618ff --- /dev/null +++ b/tests/helpers/fixtures.ts @@ -0,0 +1,58 @@ +import type { Database } from "@/lib/client/supabase/types"; +import type { SupabaseClient } from "@supabase/supabase-js"; + +type TablesInsert = + Database["public"]["Tables"][T]["Insert"]; + +export async function createTestUser( + client: SupabaseClient, + overrides?: Partial> +): Promise & { id: string }> { + const user: TablesInsert<"Users"> = { + id: overrides?.id || `test-user-${Date.now()}`, + email: overrides?.email || `test-${Date.now()}@example.com`, + role: overrides?.role || null, + ...overrides, + }; + + const { data, error } = await client + .from("Users") + .insert(user) + .select() + .single(); + + if (error) { + throw new Error(`Failed to create test user: ${error.message}`); + } + + return data; +} + +export async function createTestVolunteer( + client: SupabaseClient, + overrides?: Partial> +): Promise & { id: number }> { + const volunteer: TablesInsert<"Volunteers"> = { + name_org: overrides?.name_org || `Test Volunteer ${Date.now()}`, + email: overrides?.email || `volunteer-${Date.now()}@example.com`, + phone: overrides?.phone || null, + position: overrides?.position || null, + pronouns: overrides?.pronouns || null, + pseudonym: overrides?.pseudonym || null, + notes: overrides?.notes || null, + opt_in_communication: overrides?.opt_in_communication || null, + ...overrides, + }; + + const { data, error } = await client + .from("Volunteers") + .insert(volunteer) + .select() + .single(); + + if (error) { + throw new Error(`Failed to create test volunteer: ${error.message}`); + } + + return data; +} diff --git a/tests/helpers/index.ts b/tests/helpers/index.ts new file mode 100644 index 0000000..0938730 --- /dev/null +++ b/tests/helpers/index.ts @@ -0,0 +1,2 @@ +export * from "./supabase"; +export * from "./fixtures"; diff --git a/tests/helpers/supabase.ts b/tests/helpers/supabase.ts new file mode 100644 index 0000000..669fa75 --- /dev/null +++ b/tests/helpers/supabase.ts @@ -0,0 +1,67 @@ +import { createClient as createSupabaseClient } from "@supabase/supabase-js"; +import type { Database } from "@/lib/client/supabase/types"; +import type { SupabaseClient } from "@supabase/supabase-js"; + +type TableName = keyof Database["public"]["Tables"]; + +function getTestConfig(): { apiUrl: string; key: string } { + const apiUrl: string = process.env["API_URL"]!; + + if (!apiUrl) throw new Error("API_URL is not set"); + + const key: string = process.env["PUBLISHABLE_KEY"]!; + + if (!key) throw new Error("PUBLISHABLE_KEY is not set"); + + return { apiUrl, key }; +} + +export function createTestClient(): SupabaseClient { + const { apiUrl, key } = getTestConfig(); + return createSupabaseClient(apiUrl, key, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }); +} + +export function createAnonTestClient(): SupabaseClient { + const { apiUrl, key } = getTestConfig(); + + return createSupabaseClient(apiUrl, key, { + auth: { + autoRefreshToken: false, + persistSession: false, + }, + }); +} + +export async function deleteAllFromTables( + client: SupabaseClient +): Promise { + const deleteOrder: TableName[] = [ + "VolunteerCohorts", + "VolunteerRoles", + "Volunteers", + "Cohorts", + "Roles", + "Users", + ]; + + for (const tableName of deleteOrder) { + const { data: allRows } = await client.from(tableName).select("id"); + + if (allRows && allRows.length > 0) { + const rows = allRows as unknown as Array<{ id: number }>; + const ids = rows.map((row) => row.id); + const filteredIds = ids.filter( + (id): id is number => id !== undefined && id !== null + ); + + if (filteredIds.length > 0) { + await client.from(tableName).delete().in("id", filteredIds); + } + } + } +} diff --git a/tests/lib/api/getExample.integration.test.ts b/tests/lib/api/getExample.integration.test.ts new file mode 100644 index 0000000..b60dd43 --- /dev/null +++ b/tests/lib/api/getExample.integration.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { + createTestClient, + createTestVolunteer, + deleteAllFromTables, +} from "../../helpers"; + +describe("getExample - Integration Tests", () => { + const client = createTestClient(); + + it("should fetch volunteers from the database", async () => { + await deleteAllFromTables(client); + + const volunteer1 = await createTestVolunteer(client, { + name_org: "Test Volunteer 1", + email: "volunteer1@test.com", + }); + + const volunteer2 = await createTestVolunteer(client, { + name_org: "Test Volunteer 2", + email: "volunteer2@test.com", + }); + + const { data, error } = await client.from("Volunteers").select(); + + expect(error).toBeNull(); + expect(data).toBeDefined(); + expect(data?.length).toBeGreaterThanOrEqual(2); + expect(data?.some((v: { id: number }) => v.id === volunteer1.id)).toBe( + true + ); + expect(data?.some((v: { id: number }) => v.id === volunteer2.id)).toBe( + true + ); + }); + + it("should return empty array when no volunteers exist", async () => { + await deleteAllFromTables(client); + + const { data, error } = await client.from("Volunteers").select(); + + expect(error).toBeNull(); + expect(data).toEqual([]); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..82b1d35 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,15 @@ +import { beforeAll, afterAll } from "vitest"; +import { config } from "dotenv"; +import { createTestClient, deleteAllFromTables } from "./helpers/supabase"; + +config(); + +beforeAll(async () => { + const client = createTestClient(); + await deleteAllFromTables(client); +}); + +afterAll(async () => { + const client = createTestClient(); + await deleteAllFromTables(client); +}); diff --git a/vitest.config.ts b/vitest.config.ts index aba80f1..5208a43 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -5,6 +5,7 @@ export default defineConfig({ test: { environment: "node", globals: false, + setupFiles: ["./tests/setup.ts"], }, resolve: { alias: {