From e284a6c40920314d5ab97a9bca63a477c392a3cd Mon Sep 17 00:00:00 2001 From: duskette Date: Wed, 28 Jan 2026 21:18:51 -0500 Subject: [PATCH] initial commit --- .gitignore | 11 ++ README.md | 3 + api/.bruno/Create Comment.bru | 26 ++++ api/.bruno/Create Post.bru | 23 +++ api/.bruno/Delete Comment.bru | 21 +++ api/.bruno/Delete Post.bru | 20 +++ api/.bruno/Get Comments.bru | 20 +++ api/.bruno/Get Post.bru | 20 +++ api/.bruno/Get Posts.bru | 16 ++ api/.bruno/Upload File.bru | 20 +++ api/.bruno/bruno.json | 9 ++ api/.bruno/collection.bru | 7 + api/.bruno/environments/env.bru | 3 + api/README.md | 3 + api/bun.lock | 257 ++++++++++++++++++++++++++++++++ api/package.json | 21 +++ api/prisma.config.ts | 12 ++ api/prisma/schema.prisma | 81 ++++++++++ api/src/index.ts | 14 ++ api/src/routes/auth.ts | 100 +++++++++++++ api/src/routes/posts.ts | 228 ++++++++++++++++++++++++++++ api/src/util/env.ts | 18 +++ api/src/util/prisma.ts | 8 + api/tsconfig.json | 7 + 24 files changed, 948 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 api/.bruno/Create Comment.bru create mode 100644 api/.bruno/Create Post.bru create mode 100644 api/.bruno/Delete Comment.bru create mode 100644 api/.bruno/Delete Post.bru create mode 100644 api/.bruno/Get Comments.bru create mode 100644 api/.bruno/Get Post.bru create mode 100644 api/.bruno/Get Posts.bru create mode 100644 api/.bruno/Upload File.bru create mode 100644 api/.bruno/bruno.json create mode 100644 api/.bruno/collection.bru create mode 100644 api/.bruno/environments/env.bru create mode 100644 api/README.md create mode 100644 api/bun.lock create mode 100644 api/package.json create mode 100644 api/prisma.config.ts create mode 100644 api/prisma/schema.prisma create mode 100644 api/src/index.ts create mode 100644 api/src/routes/auth.ts create mode 100644 api/src/routes/posts.ts create mode 100644 api/src/util/env.ts create mode 100644 api/src/util/prisma.ts create mode 100644 api/tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3600752 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# deps +node_modules/ + +# environment +.env + +# api files +spark/ + +# macos +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5a510dc --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +spark +--- +a simple image-based social platform \ No newline at end of file diff --git a/api/.bruno/Create Comment.bru b/api/.bruno/Create Comment.bru new file mode 100644 index 0000000..ba27047 --- /dev/null +++ b/api/.bruno/Create Comment.bru @@ -0,0 +1,26 @@ +meta { + name: Create Comment + type: http + seq: 7 +} + +post { + url: http://localhost:3000/posts/:post_id/comments + body: json + auth: inherit +} + +params:path { + post_id: +} + +body:json { + { + "text": "hey!!" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/.bruno/Create Post.bru b/api/.bruno/Create Post.bru new file mode 100644 index 0000000..a456338 --- /dev/null +++ b/api/.bruno/Create Post.bru @@ -0,0 +1,23 @@ +meta { + name: Create Post + type: http + seq: 3 +} + +post { + url: http://localhost:3000/posts + body: json + auth: inherit +} + +body:json { + { + "description": "", + "file_id": "" + } +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/.bruno/Delete Comment.bru b/api/.bruno/Delete Comment.bru new file mode 100644 index 0000000..5830852 --- /dev/null +++ b/api/.bruno/Delete Comment.bru @@ -0,0 +1,21 @@ +meta { + name: Delete Comment + type: http + seq: 9 +} + +delete { + url: http://localhost:3000/posts/:post_id/comments/:comment_id + body: none + auth: inherit +} + +params:path { + comment_id: + post_id: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/.bruno/Delete Post.bru b/api/.bruno/Delete Post.bru new file mode 100644 index 0000000..8bfdc1d --- /dev/null +++ b/api/.bruno/Delete Post.bru @@ -0,0 +1,20 @@ +meta { + name: Delete Post + type: http + seq: 6 +} + +delete { + url: http://localhost:3000/posts/:id + body: none + auth: inherit +} + +params:path { + id: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/.bruno/Get Comments.bru b/api/.bruno/Get Comments.bru new file mode 100644 index 0000000..5552b34 --- /dev/null +++ b/api/.bruno/Get Comments.bru @@ -0,0 +1,20 @@ +meta { + name: Get Comments + type: http + seq: 8 +} + +get { + url: http://localhost:3000/posts/:post_id/comments + body: none + auth: inherit +} + +params:path { + post_id: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/.bruno/Get Post.bru b/api/.bruno/Get Post.bru new file mode 100644 index 0000000..08c531c --- /dev/null +++ b/api/.bruno/Get Post.bru @@ -0,0 +1,20 @@ +meta { + name: Get Post + type: http + seq: 4 +} + +get { + url: http://localhost:3000/posts/:id + body: none + auth: inherit +} + +params:path { + id: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/.bruno/Get Posts.bru b/api/.bruno/Get Posts.bru new file mode 100644 index 0000000..c554345 --- /dev/null +++ b/api/.bruno/Get Posts.bru @@ -0,0 +1,16 @@ +meta { + name: Get Posts + type: http + seq: 2 +} + +get { + url: http://localhost:3000/posts + body: none + auth: inherit +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/.bruno/Upload File.bru b/api/.bruno/Upload File.bru new file mode 100644 index 0000000..8eb1b57 --- /dev/null +++ b/api/.bruno/Upload File.bru @@ -0,0 +1,20 @@ +meta { + name: Upload File + type: http + seq: 3 +} + +post { + url: http://localhost:3000/posts/file + body: multipartForm + auth: inherit +} + +body:multipart-form { + image: +} + +settings { + encodeUrl: true + timeout: 0 +} diff --git a/api/.bruno/bruno.json b/api/.bruno/bruno.json new file mode 100644 index 0000000..8e4a978 --- /dev/null +++ b/api/.bruno/bruno.json @@ -0,0 +1,9 @@ +{ + "version": "1", + "name": "Spark", + "type": "collection", + "ignore": [ + "node_modules", + ".git" + ] +} \ No newline at end of file diff --git a/api/.bruno/collection.bru b/api/.bruno/collection.bru new file mode 100644 index 0000000..3a3605b --- /dev/null +++ b/api/.bruno/collection.bru @@ -0,0 +1,7 @@ +headers { + Authorization: {{TOKEN}} +} + +auth { + mode: none +} diff --git a/api/.bruno/environments/env.bru b/api/.bruno/environments/env.bru new file mode 100644 index 0000000..a2d4780 --- /dev/null +++ b/api/.bruno/environments/env.bru @@ -0,0 +1,3 @@ +vars:secret [ + TOKEN +] diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..c6d4516 --- /dev/null +++ b/api/README.md @@ -0,0 +1,3 @@ +spark/api +--- +spark's backend \ No newline at end of file diff --git a/api/bun.lock b/api/bun.lock new file mode 100644 index 0000000..bc7bb43 --- /dev/null +++ b/api/bun.lock @@ -0,0 +1,257 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "spark-api", + "dependencies": { + "@prisma/adapter-pg": "^7.3.0", + "@prisma/client": "^7.3.0", + "hono": "^4.11.7", + "pg": "^8.17.2", + "zod": "^4.1.12", + }, + "devDependencies": { + "@types/bun": "latest", + "prisma": "^7.3.0", + }, + }, + }, + "packages": { + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@10.5.0", "", { "dependencies": { "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw=="], + + "@chevrotain/gast": ["@chevrotain/gast@10.5.0", "", { "dependencies": { "@chevrotain/types": "10.5.0", "lodash": "4.17.21" } }, "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A=="], + + "@chevrotain/types": ["@chevrotain/types@10.5.0", "", {}, "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A=="], + + "@chevrotain/utils": ["@chevrotain/utils@10.5.0", "", {}, "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ=="], + + "@electric-sql/pglite": ["@electric-sql/pglite@0.3.15", "", {}, "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ=="], + + "@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.0.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg=="], + + "@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.2.20", "", { "peerDependencies": { "@electric-sql/pglite": "0.3.15" } }, "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A=="], + + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + + "@hono/zod-validator": ["@hono/zod-validator@0.7.4", "", { "peerDependencies": { "hono": "^4", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-biKGn3BRJVaftZlIPMyK+HCe/UHAjJ6sH0UyXe3+v0OcgVr9xfImDROTJFLtn9e3XEEAHGZIM9U6evu85abm8Q=="], + + "@mrleebo/prisma-ast": ["@mrleebo/prisma-ast@0.13.1", "", { "dependencies": { "chevrotain": "^10.5.0", "lilconfig": "^2.1.0" } }, "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw=="], + + "@prisma/adapter-pg": ["@prisma/adapter-pg@7.3.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.3.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-iuYQMbIPO6i9O45Fv8TB7vWu00BXhCaNAShenqF7gLExGDbnGp5BfFB4yz1K59zQ59jF6tQ9YHrg0P6/J3OoLg=="], + + "@prisma/client": ["@prisma/client@7.3.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.3.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" }, "optionalPeers": ["prisma", "typescript"] }, "sha512-FXBIxirqQfdC6b6HnNgxGmU7ydCPEPk7maHMOduJJfnTP+MuOGa15X4omjR/zpPUUpm8ef/mEFQjJudOGkXFcQ=="], + + "@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.3.0", "", {}, "sha512-dG/ceD9c+tnXATPk8G+USxxYM9E6UdMTnQeQ+1SZUDxTz7SgQcfxEqafqIQHcjdlcNK/pvmmLfSwAs3s2gYwUw=="], + + "@prisma/config": ["@prisma/config@7.3.0", "", { "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", "effect": "3.18.4", "empathic": "2.0.0" } }, "sha512-QyMV67+eXF7uMtKxTEeQqNu/Be7iH+3iDZOQZW5ttfbSwBamCSdwPszA0dum+Wx27I7anYTPLmRmMORKViSW1A=="], + + "@prisma/debug": ["@prisma/debug@7.3.0", "", {}, "sha512-yh/tHhraCzYkffsI1/3a7SHX8tpgbJu1NPnuxS4rEpJdWAUDHUH25F1EDo6PPzirpyLNkgPPZdhojQK804BGtg=="], + + "@prisma/dev": ["@prisma/dev@0.20.0", "", { "dependencies": { "@electric-sql/pglite": "0.3.15", "@electric-sql/pglite-socket": "0.0.20", "@electric-sql/pglite-tools": "0.2.20", "@hono/node-server": "1.19.9", "@mrleebo/prisma-ast": "0.13.1", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "4.11.4", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ=="], + + "@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-Wdlezh1ck0Rq2dDINkfSkwbR53q53//Eo1vVqVLwtiZ0I6fuWDGNPxwq+SNAIHnsU+FD/m3aIJKevH3vF13U3w=="], + + "@prisma/engines": ["@prisma/engines@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0", "@prisma/engines-version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", "@prisma/fetch-engine": "7.3.0", "@prisma/get-platform": "7.3.0" } }, "sha512-cWRQoPDXPtR6stOWuWFZf9pHdQ/o8/QNWn0m0zByxf5Kd946Q875XdEJ52pEsX88vOiXUmjuPG3euw82mwQNMg=="], + + "@prisma/engines-version": ["@prisma/engines-version@7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", "", {}, "sha512-IH2va2ouUHihyiTTRW889LjKAl1CusZOvFfZxCDNpjSENt7g2ndFsK0vdIw/72v7+jCN6YgkHmdAP/BI7SDgyg=="], + + "@prisma/fetch-engine": ["@prisma/fetch-engine@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0", "@prisma/engines-version": "7.3.0-16.9d6ad21cbbceab97458517b147a6a09ff43aa735", "@prisma/get-platform": "7.3.0" } }, "sha512-Mm0F84JMqM9Vxk70pzfNpGJ1lE4hYjOeLMu7nOOD1i83nvp8MSAcFYBnHqLvEZiA6onUR+m8iYogtOY4oPO5lQ=="], + + "@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="], + + "@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="], + + "@prisma/studio-core": ["@prisma/studio-core@0.13.1", "", { "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="], + + "@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], + + "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], + + "aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="], + + "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], + + "c12": ["c12@3.1.0", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^16.6.1", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.4.2", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^1.0.0", "pkg-types": "^2.2.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw=="], + + "chevrotain": ["chevrotain@10.5.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "10.5.0", "@chevrotain/gast": "10.5.0", "@chevrotain/types": "10.5.0", "@chevrotain/utils": "10.5.0", "lodash": "4.17.21", "regexp-to-ast": "0.5.0" } }, "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A=="], + + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + + "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], + + "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], + + "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="], + + "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], + + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], + + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "effect": ["effect@3.18.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA=="], + + "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], + + "exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="], + + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="], + + "get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="], + + "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="], + + "graphmatch": ["graphmatch@1.1.0", "", {}, "sha512-0E62MaTW5rPZVRLyIJZG/YejmdA/Xr1QydHEw3Vt+qOKkMIOE8WDLc9ZX2bmAjtJFZcId4lEdrdmASsEy7D1QA=="], + + "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], + + "http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "lilconfig": ["lilconfig@2.1.0", "", {}, "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "lru.min": ["lru.min@1.1.3", "", {}, "sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q=="], + + "mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="], + + "named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="], + + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + + "nypm": ["nypm@0.6.4", "", { "dependencies": { "citty": "^0.2.0", "pathe": "^2.0.3", "tinyexec": "^1.0.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw=="], + + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], + + "pg": ["pg@8.17.2", "", { "dependencies": { "pg-connection-string": "^2.10.1", "pg-pool": "^3.11.0", "pg-protocol": "^1.11.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-vjbKdiBJRqzcYw1fNU5KuHyYvdJ1qpcQg1CeBrHFqV1pWgHeVR6j/+kX0E1AAXfyuLUGY1ICrN2ELKA/z2HWzw=="], + + "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], + + "pg-connection-string": ["pg-connection-string@2.10.1", "", {}, "sha512-iNzslsoeSH2/gmDDKiyMqF64DATUCWj3YJ0wP14kqcsf2TUklwimd+66yYojKwZCA7h2yRNLGug71hCBA2a4sw=="], + + "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], + + "pg-pool": ["pg-pool@3.11.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w=="], + + "pg-protocol": ["pg-protocol@1.11.0", "", {}, "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g=="], + + "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], + + "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], + + "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + + "postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="], + + "postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="], + + "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], + + "postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="], + + "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], + + "prisma": ["prisma@7.3.0", "", { "dependencies": { "@prisma/config": "7.3.0", "@prisma/dev": "0.20.0", "@prisma/engines": "7.3.0", "@prisma/studio-core": "0.13.1", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3", "typescript"], "bin": { "prisma": "build/index.js" } }, "sha512-ApYSOLHfMN8WftJA+vL6XwAPOh/aZ0BgUyyKPwUFgjARmG6EBI9LzDPf6SWULQMSAxydV9qn5gLj037nPNlg2w=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + + "regexp-to-ast": ["regexp-to-ast@0.5.0", "", {}, "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw=="], + + "remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + + "zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="], + + "@prisma/dev/hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="], + + "@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="], + + "@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.3.0", "", { "dependencies": { "@prisma/debug": "7.3.0" } }, "sha512-N7c6m4/I0Q6JYmWKP2RCD/sM9eWiyCPY98g5c0uEktObNSZnugW2U/PO+pwL0UaqzxqTXt7gTsYsb0FnMnJNbg=="], + + "@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="], + + "nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], + + "pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], + + "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], + } +} diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..c9808fc --- /dev/null +++ b/api/package.json @@ -0,0 +1,21 @@ +{ + "name": "spark-api", + "scripts": { + "dev": "bun run --hot src/index.ts", + "db:gen": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:push": "prisma db push" + }, + "dependencies": { + "@hono/zod-validator": "^0.7.4", + "@prisma/adapter-pg": "^7.3.0", + "@prisma/client": "^7.3.0", + "hono": "^4.11.7", + "pg": "^8.17.2", + "zod": "^4.1.12" + }, + "devDependencies": { + "@types/bun": "latest", + "prisma": "^7.3.0" + } +} \ No newline at end of file diff --git a/api/prisma.config.ts b/api/prisma.config.ts new file mode 100644 index 0000000..d5ecde5 --- /dev/null +++ b/api/prisma.config.ts @@ -0,0 +1,12 @@ +// This file was generated by Prisma, and assumes you run Prisma commands using `bun --bun run prisma [command]`. +import { defineConfig, env } from "prisma/config"; + +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/api/prisma/schema.prisma b/api/prisma/schema.prisma new file mode 100644 index 0000000..cbddd9f --- /dev/null +++ b/api/prisma/schema.prisma @@ -0,0 +1,81 @@ +generator client { + provider = "prisma-client-js" + output = "../node_modules/.prisma/client/" +} + +datasource db { + provider = "postgresql" +} + +model User { + id String @id + username String @unique + + posts Post[] + comments Comment[] + notifications Notification[] + sessions Session[] + + @@map("users") +} + +model Post { + id Int @id @default(autoincrement()) + created DateTime @default(now()) + + description String? + images Image[] + comments Comment[] + + author User? @relation(fields: [author_id], references: [id], onDelete: Cascade) + author_id String? + + @@map("posts") +} + +model Image { + id Int @id @default(autoincrement()) + filename String + + post Post @relation(fields: [post_id], references: [id], onDelete: Cascade) + post_id Int + + @@map("images") +} + +model Comment { + id Int @id @default(autoincrement()) + created DateTime @default(now()) + + text String + + post Post @relation(fields: [post_id], references: [id], onDelete: Cascade) + post_id Int + + author User @relation(fields: [author_id], references: [id], onDelete: Cascade) + author_id String + + @@map("comments") +} + +model Notification { + id Int @id @default(autoincrement()) + text String + href String? + + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) + user_id String + + @@map("notifications") +} + +model Session { + token String @id + + user User @relation(fields: [user_id], references: [id], onDelete: Cascade) + user_id String + + created DateTime @default(now()) + + @@map("sessions") +} \ No newline at end of file diff --git a/api/src/index.ts b/api/src/index.ts new file mode 100644 index 0000000..a360cb8 --- /dev/null +++ b/api/src/index.ts @@ -0,0 +1,14 @@ +import { Hono } from 'hono' +import { AuthGuard, AuthRoute } from './routes/auth' +import { PostsRoute } from './routes/posts' + +const app = new Hono() + + .get('/', (c) => { + return c.text('Hello Hono!') + }) + + .route('/auth', AuthRoute) + .route('/posts', PostsRoute) + +export default app diff --git a/api/src/routes/auth.ts b/api/src/routes/auth.ts new file mode 100644 index 0000000..658e9ab --- /dev/null +++ b/api/src/routes/auth.ts @@ -0,0 +1,100 @@ +import { User } from '@prisma/client'; +import { Hono } from 'hono'; +import { createMiddleware } from 'hono/factory'; +import { getCookie, setCookie } from 'hono/cookie'; +import { randomBytes } from 'node:crypto'; +import { env } from '../util/env'; +import { prisma } from '../util/prisma'; +import { zValidator } from '@hono/zod-validator'; +import z from 'zod'; + +export const AUTH_URL = env.OAUTH_AUTHORIZE_URL + '?' + new URLSearchParams({ + redirect_uri: env.OAUTH_REDIRECT_URI +}); + +export const AuthGuard = createMiddleware<{ + Variables: { + user: User + } +}>(async (c, next) => { + const token = getCookie(c, env.AUTH_COOKIE_NAME) ?? c.req.header('Authorization')?.replace('Bearer ', ''); + if (!token) return c.json({ href: AUTH_URL }, 401); + + const user = await prisma.user.findFirst({ + where: { + sessions: { some: { token } } + } + }); + if (!user) return c.json({ href: AUTH_URL }, 401); + + c.set('user', user); + await next(); +}); + +async function authorize(code: string) { + const res = await fetch(env.OAUTH_VERIFY_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + redirect_uri: env.OAUTH_REDIRECT_URI, + code + }), + }); + if (!res.ok) throw new Error("OAuth request failed"); + + return (await res.json()) as { + user_id: string; + username: string; + }; +} + +async function login(code: string) { + const passport = await authorize(code); + + const user = prisma.user.upsert({ + where: { id: passport.user_id }, + update: {}, + create: { + id: passport.user_id, + username: passport.username + } + }); + + return user; +} + +const AuthRoute = new Hono() + + .get('/login', + zValidator('query', z.object({ + code: z.string() + })), + async (c) => { + const { code } = c.req.valid('query'); + if (!code) return c.text('Missing OAuth code', 400); + + const user = await login(code); + + const session = await prisma.session.create({ + data: { + user_id: user.id, + token: randomBytes(64).toString('hex') + } + }); + + setCookie(c, env.AUTH_COOKIE_NAME, session.token, { + path: "/", + secure: env.OAUTH_REDIRECT_URI.startsWith('https://') + }); + + return c.json({ + token: session.token, + user_id: session.user_id, + created: session.created + }); + } + ) + +export { AuthRoute }; \ No newline at end of file diff --git a/api/src/routes/posts.ts b/api/src/routes/posts.ts new file mode 100644 index 0000000..1fa7b1d --- /dev/null +++ b/api/src/routes/posts.ts @@ -0,0 +1,228 @@ +import { Hono } from 'hono'; +import { zValidator } from '@hono/zod-validator'; +import { prisma } from '../util/prisma'; +import z from 'zod'; +import { randomBytes } from 'node:crypto'; +import * as path from 'node:path'; +import { AuthGuard } from './auth'; +import { env } from '../util/env'; + +const pendingFiles: { id: string, file: File, expires: Date }[] = []; + +function removePendingFile(id: string) { + pendingFiles.splice(pendingFiles.findIndex(f => f.id === id), 1); +} + +function cullPendingFiles() { + for (const file of pendingFiles) { + if (file.expires.getTime() < Date.now()) removePendingFile(file.id); + } +} + +function getMIME(mime: string) { + return env.ALLOWED_MIME.find(m => m.type === mime); +} + +const PostsRoute = new Hono() + + .use(AuthGuard) + + .get('/', + zValidator('query', z.object({ + count: z.number().default(20), + before_post: z.number().optional() + })), + async (c) => { + const { + count, + before_post + } = c.req.valid('query'); + + const posts = await prisma.post.findMany({ + where: { + id: before_post ? { lt: before_post } : undefined, + }, + include: { images: true, author: true }, + take: count + }); + + return c.json({ posts }); + } + ) + + .get('/:post_id', + zValidator('param', z.object({ + post_id: z.string().transform(s => +s).pipe(z.number()) + })), + async (c) => { + const { post_id } = c.req.valid('param'); + + const post = await prisma.post.findUnique({ + where: { id: post_id }, + include: { images: true, author: true } + }); + if (!post) return c.text("Unknown post", 404); + + return c.json(post); + } + ) + + .get('/:post_id/comments', + zValidator('param', z.object({ + post_id: z.string().transform(s => +s).pipe(z.number()) + })), + async (c) => { + const { post_id } = c.req.valid('param'); + + const post = await prisma.post.findUnique({ + where: { id: post_id }, + select: { comments: { include: { author: true } } }, + }); + if (!post) return c.text("Unknown post", 404); + + return c.json({ comments: post.comments }); + } + ) + + .post('/:post_id/comments', + zValidator('param', z.object({ + post_id: z.string().transform(s => +s).pipe(z.number()) + })), + zValidator('json', z.object({ + text: z.string().max(500).nonempty() + })), + async (c) => { + const { post_id } = c.req.valid('param'); + const { text } = c.req.valid('json'); + const user = c.get('user'); + + try { + const comment = await prisma.comment.create({ + data: { + text, + post_id, + author_id: user.id + }, + include: { author: true } + }); + + return c.json(comment); + } catch { + return c.text("Unknown post", 404); + } + } + ) + + .delete('/:post_id/comments/:comment_id', + zValidator('param', z.object({ + post_id: z.string().transform(s => +s).pipe(z.number()), + comment_id: z.string().transform(s => +s).pipe(z.number()) + })), + async (c) => { + const { post_id, comment_id } = c.req.valid('param'); + const user = c.get('user'); + + try { + const comment = await prisma.comment.delete({ + where: { + id: comment_id, + post_id, + author_id: user.id + } + }); + + return c.text(`Deleted comment ${comment.id}`); + } catch { + return c.text("Unknown comment", 404); + } + } + ) + + .post('/', + zValidator('json', z.object({ + description: z.string().max(500).nonempty().optional(), + file_id: z.string().optional() + })), + async (c) => { + cullPendingFiles(); + + const { + description, + file_id + } = c.req.valid('json'); + const user = c.get('user'); + + let post = await prisma.post.create({ + data: { + author_id: user.id, + description + } + }); + + let image; + if (file_id) { + const file = pendingFiles.find(f => f.id === file_id); + if (!file) return c.text("Invalid image ID"); + + const data = file.file; + const mime = getMIME(data.type); + if (!mime) return c.text("Something went wrong identifying the file type of your image", 500); + + const filename = `${post.id}-${file.id}.${mime.ext}`; + removePendingFile(file.id); + + Bun.write( + path.join(env.FILE_DIR, `./images/${user.id}/${filename}`), + data + ); + + image = await prisma.image.create({ + data: { + filename, + post_id: post.id + } + }); + } + + return c.json({ + ...post, + images: image ? [image] : [] + }) + } + ) + + .post('/file', async (c) => { + const id = randomBytes(10).toString('hex'); + const expires = new Date(Date.now() + 300000); // 5 min + const body = await c.req.parseBody(); + + const file = body['image']; + if (file instanceof File && getMIME(file.type)) { + pendingFiles.push({ id, expires, file }); + return c.json({ id, expires }); + } else { + return c.text("Invalid file body"); + } + }) + + .delete('/:post_id', + zValidator('param', z.object({ + post_id: z.string().transform(s => +s).pipe(z.number()) + })), + async (c) => { + const { post_id } = c.req.valid('param'); + const user = c.get('user'); + + try { + const post = await prisma.post.delete({ + where: { id: post_id, author_id: user.id } + }); + + return c.text(`Deleted post ${post.id}`); + } catch { + return c.text("Unknown post", 404); + } + } + ) + +export { PostsRoute }; \ No newline at end of file diff --git a/api/src/util/env.ts b/api/src/util/env.ts new file mode 100644 index 0000000..97896c9 --- /dev/null +++ b/api/src/util/env.ts @@ -0,0 +1,18 @@ +import z from 'zod'; + +const envSchema = z.object({ + DATABASE_URL: z.string(), + FILE_DIR: z.string(), + ALLOWED_MIME: z.string().transform(s => s.replace(' ', '').split(',').map(m => { + const [type, ext] = m.split('='); + return { type, ext }; + })), + + OAUTH_AUTHORIZE_URL: z.string(), + OAUTH_REDIRECT_URI: z.string(), + OAUTH_VERIFY_URL: z.string(), + + AUTH_COOKIE_NAME: z.string().default('spark-token') +}); + +export const env = envSchema.parse(Bun.env); \ No newline at end of file diff --git a/api/src/util/prisma.ts b/api/src/util/prisma.ts new file mode 100644 index 0000000..6404783 --- /dev/null +++ b/api/src/util/prisma.ts @@ -0,0 +1,8 @@ +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '@prisma/client'; +import { env } from './env'; + +const adapter = new PrismaPg({ connectionString: env.DATABASE_URL }); +const prisma = new PrismaClient({ adapter }); + +export { prisma }; \ No newline at end of file diff --git a/api/tsconfig.json b/api/tsconfig.json new file mode 100644 index 0000000..c442b33 --- /dev/null +++ b/api/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "strict": true, + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} \ No newline at end of file