v0.1.20 - PostgreSQL 전환 및 Docker Compose 초안 추가
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
PORT=3001
|
||||
DB_FILE=./data/planner.sqlite
|
||||
DATABASE_URL=postgresql://planner:planner1234@localhost:5432/ten_minute_planner
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
|
||||
14
backend/Dockerfile
Normal file
14
backend/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
COPY src ./src
|
||||
COPY .env.example ./.env.example
|
||||
COPY drizzle.config.js ./drizzle.config.js
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
CMD ["node", "src/server.js"]
|
||||
@@ -10,8 +10,8 @@ config({ path: path.join(__dirname, '.env') })
|
||||
export default {
|
||||
schema: './src/db/schema.js',
|
||||
out: './drizzle',
|
||||
dialect: 'sqlite',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DB_FILE ?? './data/planner.sqlite',
|
||||
url: process.env.DATABASE_URL ?? 'postgresql://planner:planner1234@localhost:5432/ten_minute_planner',
|
||||
},
|
||||
}
|
||||
|
||||
241
backend/package-lock.json
generated
241
backend/package-lock.json
generated
@@ -9,10 +9,10 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^10.0.1",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"fastify": "^5.2.1",
|
||||
"pg": "^8.13.3",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1083,7 +1083,9 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "11.10.0",
|
||||
@@ -1091,6 +1093,8 @@
|
||||
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bindings": "^1.5.0",
|
||||
"prebuild-install": "^7.1.1"
|
||||
@@ -1101,6 +1105,8 @@
|
||||
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
|
||||
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"file-uri-to-path": "1.0.0"
|
||||
}
|
||||
@@ -1110,6 +1116,8 @@
|
||||
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
@@ -1135,6 +1143,8 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
@@ -1151,7 +1161,9 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
@@ -1189,6 +1201,8 @@
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
@@ -1204,6 +1218,8 @@
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
@@ -1222,6 +1238,8 @@
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -1377,6 +1395,8 @@
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
|
||||
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
@@ -1451,6 +1471,8 @@
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -1578,7 +1600,9 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
|
||||
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/find-my-way": {
|
||||
"version": "9.5.0",
|
||||
@@ -1598,7 +1622,9 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/gel": {
|
||||
"version": "2.2.0",
|
||||
@@ -1638,7 +1664,9 @@
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
@@ -1658,19 +1686,25 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ini": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "2.3.0",
|
||||
@@ -1758,6 +1792,8 @@
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -1770,6 +1806,8 @@
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
@@ -1778,7 +1816,9 @@
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/mnemonist": {
|
||||
"version": "0.40.0",
|
||||
@@ -1800,13 +1840,17 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.89.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
|
||||
"integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
@@ -1834,10 +1878,101 @@
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.12.0",
|
||||
"pg-pool": "^3.13.0",
|
||||
"pg-protocol": "^1.13.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
|
||||
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
|
||||
"integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
|
||||
"integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
|
||||
"integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz",
|
||||
@@ -1875,12 +2010,53 @@
|
||||
"integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
@@ -1923,6 +2099,8 @@
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
|
||||
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
@@ -1939,6 +2117,8 @@
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
@@ -1954,6 +2134,8 @@
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
@@ -2034,7 +2216,9 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/safe-regex2": {
|
||||
"version": "5.1.1",
|
||||
@@ -2132,7 +2316,9 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
@@ -2153,6 +2339,8 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
@@ -2203,6 +2391,8 @@
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
@@ -2212,6 +2402,8 @@
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -2221,6 +2413,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
|
||||
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
@@ -2233,6 +2427,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
@@ -2270,6 +2466,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
@@ -2281,7 +2479,9 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "4.0.0",
|
||||
@@ -2303,7 +2503,18 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^10.0.1",
|
||||
"better-sqlite3": "^11.8.1",
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-orm": "^0.39.1",
|
||||
"fastify": "^5.2.1",
|
||||
"pg": "^8.13.3",
|
||||
"zod": "^3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -10,7 +10,7 @@ config({ path: path.join(__dirname, '..', '.env') })
|
||||
|
||||
const envSchema = z.object({
|
||||
PORT: z.coerce.number().default(3001),
|
||||
DB_FILE: z.string().default('./data/planner.sqlite'),
|
||||
DATABASE_URL: z.string().min(1).default('postgresql://planner:planner1234@localhost:5432/ten_minute_planner'),
|
||||
CORS_ORIGIN: z.string().default('http://localhost:5173'),
|
||||
SESSION_TTL_DAYS: z.coerce.number().default(30),
|
||||
})
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import Database from 'better-sqlite3'
|
||||
import { drizzle } from 'drizzle-orm/better-sqlite3'
|
||||
import { drizzle } from 'drizzle-orm/node-postgres'
|
||||
import pg from 'pg'
|
||||
import { env } from '../config.js'
|
||||
import * as schema from './schema.js'
|
||||
|
||||
function ensureDatabaseDirectory(dbFile) {
|
||||
const absoluteDbPath = path.resolve(dbFile)
|
||||
fs.mkdirSync(path.dirname(absoluteDbPath), { recursive: true })
|
||||
return absoluteDbPath
|
||||
}
|
||||
const { Pool } = pg
|
||||
|
||||
const sqlite = new Database(ensureDatabaseDirectory(env.DB_FILE))
|
||||
export const pool = new Pool({
|
||||
connectionString: env.DATABASE_URL,
|
||||
})
|
||||
|
||||
export const db = drizzle(sqlite, { schema })
|
||||
export { sqlite }
|
||||
export const db = drizzle(pool, { schema })
|
||||
|
||||
@@ -1,63 +1,55 @@
|
||||
import { sqlite } from './client.js'
|
||||
import { pool } from './client.js'
|
||||
|
||||
function ensureColumn(tableName, columnName, definition) {
|
||||
const columns = sqlite.prepare(`PRAGMA table_info(${tableName})`).all()
|
||||
const hasColumn = columns.some((column) => column.name === columnName)
|
||||
|
||||
if (!hasColumn) {
|
||||
sqlite.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function ensureDatabaseSchema() {
|
||||
sqlite.exec(`
|
||||
export async function ensureDatabaseSchema() {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
nickname TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
id SERIAL PRIMARY KEY,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
nickname VARCHAR(60) NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auth_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
expires_at INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash VARCHAR(255) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS auth_sessions_user_id_idx
|
||||
ON auth_sessions (user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS planner_entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
entry_date TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
entry_date VARCHAR(10) NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS planner_entries_user_date_unique
|
||||
ON planner_entries (user_id, entry_date);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS goals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
target_date TEXT NOT NULL,
|
||||
active_from TEXT,
|
||||
active_until TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
color TEXT NOT NULL DEFAULT '#1c1917',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
completed_at INTEGER,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`)
|
||||
CREATE INDEX IF NOT EXISTS planner_entries_user_id_idx
|
||||
ON planner_entries (user_id);
|
||||
|
||||
ensureColumn('goals', 'active_from', 'TEXT')
|
||||
ensureColumn('goals', 'active_until', 'TEXT')
|
||||
CREATE TABLE IF NOT EXISTS goals (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title VARCHAR(120) NOT NULL,
|
||||
target_date VARCHAR(10) NOT NULL,
|
||||
active_from VARCHAR(10),
|
||||
active_until VARCHAR(10),
|
||||
color VARCHAR(32) NOT NULL DEFAULT '#1c1917',
|
||||
created_at TIMESTAMPTZ NOT NULL,
|
||||
updated_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS goals_user_id_idx
|
||||
ON goals (user_id);
|
||||
`)
|
||||
}
|
||||
|
||||
@@ -1,47 +1,67 @@
|
||||
import { integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
|
||||
import {
|
||||
integer,
|
||||
index,
|
||||
jsonb,
|
||||
pgTable,
|
||||
serial,
|
||||
timestamp,
|
||||
uniqueIndex,
|
||||
varchar,
|
||||
} from 'drizzle-orm/pg-core'
|
||||
|
||||
export const users = sqliteTable('users', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
email: text('email').notNull().unique(),
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
nickname: text('nickname').notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
export const users = pgTable('users', {
|
||||
id: serial('id').primaryKey(),
|
||||
email: varchar('email', { length: 255 }).notNull().unique(),
|
||||
passwordHash: varchar('password_hash', { length: 255 }).notNull(),
|
||||
nickname: varchar('nickname', { length: 60 }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
|
||||
})
|
||||
|
||||
export const authSessions = sqliteTable('auth_sessions', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
tokenHash: text('token_hash').notNull().unique(),
|
||||
expiresAt: integer('expires_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
})
|
||||
|
||||
export const plannerEntries = sqliteTable(
|
||||
'planner_entries',
|
||||
export const authSessions = pgTable(
|
||||
'auth_sessions',
|
||||
{
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
entryDate: text('entry_date').notNull(),
|
||||
payload: text('payload').notNull(),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
tokenHash: varchar('token_hash', { length: 255 }).notNull().unique(),
|
||||
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
userDateUnique: uniqueIndex('planner_entries_user_date_unique').on(table.userId, table.entryDate),
|
||||
userIndex: index('auth_sessions_user_id_idx').on(table.userId),
|
||||
}),
|
||||
)
|
||||
|
||||
export const goals = sqliteTable('goals', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
title: text('title').notNull(),
|
||||
targetDate: text('target_date').notNull(),
|
||||
activeFrom: text('active_from'),
|
||||
activeUntil: text('active_until'),
|
||||
status: text('status').notNull().default('active'),
|
||||
color: text('color').notNull().default('#1c1917'),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(),
|
||||
completedAt: integer('completed_at', { mode: 'timestamp_ms' }),
|
||||
})
|
||||
export const plannerEntries = pgTable(
|
||||
'planner_entries',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
entryDate: varchar('entry_date', { length: 10 }).notNull(),
|
||||
payload: jsonb('payload').notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
userDateUnique: uniqueIndex('planner_entries_user_date_unique').on(table.userId, table.entryDate),
|
||||
userIndex: index('planner_entries_user_id_idx').on(table.userId),
|
||||
}),
|
||||
)
|
||||
|
||||
export const goals = pgTable(
|
||||
'goals',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
userId: integer('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
||||
title: varchar('title', { length: 120 }).notNull(),
|
||||
targetDate: varchar('target_date', { length: 10 }).notNull(),
|
||||
activeFrom: varchar('active_from', { length: 10 }),
|
||||
activeUntil: varchar('active_until', { length: 10 }),
|
||||
color: varchar('color', { length: 32 }).notNull().default('#1c1917'),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
userIndex: index('goals_user_id_idx').on(table.userId),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -9,7 +9,6 @@ const goalSchema = z.object({
|
||||
targetDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
activeFrom: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
||||
activeUntil: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
||||
status: z.enum(['active', 'done', 'archived']).optional(),
|
||||
color: z.string().trim().min(4).max(32).optional(),
|
||||
})
|
||||
|
||||
@@ -18,13 +17,11 @@ const goalUpdateSchema = z.object({
|
||||
targetDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
||||
activeFrom: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
||||
activeUntil: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().nullable(),
|
||||
status: z.enum(['active', 'done', 'archived']).optional(),
|
||||
color: z.string().trim().min(4).max(32).optional(),
|
||||
})
|
||||
|
||||
const goalQuerySchema = z.object({
|
||||
query: z.string().trim().optional(),
|
||||
status: z.enum(['active', 'done', 'archived', 'all']).optional(),
|
||||
})
|
||||
|
||||
async function requireAuthenticatedUser(request, reply) {
|
||||
@@ -48,10 +45,9 @@ async function validateGoalSchedule({
|
||||
userId,
|
||||
activeFrom,
|
||||
activeUntil,
|
||||
status,
|
||||
excludeGoalId = null,
|
||||
}) {
|
||||
if (!activeFrom || !activeUntil || status !== 'active') {
|
||||
if (!activeFrom || !activeUntil) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -65,7 +61,7 @@ async function validateGoalSchedule({
|
||||
return false
|
||||
}
|
||||
|
||||
if (goal.status !== 'active' || !goal.activeFrom || !goal.activeUntil) {
|
||||
if (!goal.activeFrom || !goal.activeUntil) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -92,10 +88,6 @@ export async function registerGoalRoutes(app) {
|
||||
|
||||
const filters = [eq(goals.userId, user.id)]
|
||||
|
||||
if (query.data.status && query.data.status !== 'all') {
|
||||
filters.push(eq(goals.status, query.data.status))
|
||||
}
|
||||
|
||||
if (query.data.query) {
|
||||
filters.push(like(goals.title, `%${query.data.query}%`))
|
||||
}
|
||||
@@ -141,7 +133,6 @@ export async function registerGoalRoutes(app) {
|
||||
userId: user.id,
|
||||
activeFrom: payload.data.activeFrom ?? null,
|
||||
activeUntil: payload.data.activeUntil ?? null,
|
||||
status: payload.data.status ?? 'active',
|
||||
})
|
||||
|
||||
if (overlappedGoal) {
|
||||
@@ -161,10 +152,8 @@ export async function registerGoalRoutes(app) {
|
||||
activeFrom: payload.data.activeFrom ?? null,
|
||||
activeUntil: payload.data.activeUntil ?? null,
|
||||
color: payload.data.color ?? '#1c1917',
|
||||
status: payload.data.status ?? 'active',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
completedAt: payload.data.status === 'done' ? now : null,
|
||||
})
|
||||
.returning()
|
||||
|
||||
@@ -214,8 +203,6 @@ export async function registerGoalRoutes(app) {
|
||||
|
||||
const nextActiveFrom = payload.data.activeFrom !== undefined ? payload.data.activeFrom : existingGoal.activeFrom
|
||||
const nextActiveUntil = payload.data.activeUntil !== undefined ? payload.data.activeUntil : existingGoal.activeUntil
|
||||
const nextStatus = payload.data.status ?? existingGoal.status
|
||||
|
||||
if ((nextActiveFrom && !nextActiveUntil) || (!nextActiveFrom && nextActiveUntil)) {
|
||||
return reply.code(400).send({
|
||||
message: '표시 시작일과 종료일은 함께 입력해 주세요.',
|
||||
@@ -232,7 +219,6 @@ export async function registerGoalRoutes(app) {
|
||||
userId: user.id,
|
||||
activeFrom: nextActiveFrom,
|
||||
activeUntil: nextActiveUntil,
|
||||
status: nextStatus,
|
||||
excludeGoalId: existingGoal.id,
|
||||
})
|
||||
|
||||
@@ -262,22 +248,10 @@ export async function registerGoalRoutes(app) {
|
||||
nextValues.activeUntil = payload.data.activeUntil
|
||||
}
|
||||
|
||||
if (payload.data.status !== undefined) {
|
||||
nextValues.status = payload.data.status
|
||||
}
|
||||
|
||||
if (payload.data.color !== undefined) {
|
||||
nextValues.color = payload.data.color
|
||||
}
|
||||
|
||||
if (payload.data.status === 'done' && !existingGoal.completedAt) {
|
||||
nextValues.completedAt = new Date()
|
||||
}
|
||||
|
||||
if (payload.data.status && payload.data.status !== 'done') {
|
||||
nextValues.completedAt = null
|
||||
}
|
||||
|
||||
const [goal] = await db
|
||||
.update(goals)
|
||||
.set(nextValues)
|
||||
@@ -289,4 +263,42 @@ export async function registerGoalRoutes(app) {
|
||||
goal,
|
||||
}
|
||||
})
|
||||
|
||||
app.delete('/api/goals/:goalId', async (request, reply) => {
|
||||
const user = await requireAuthenticatedUser(request, reply)
|
||||
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
|
||||
const params = z.object({
|
||||
goalId: z.coerce.number().int().positive(),
|
||||
}).safeParse(request.params)
|
||||
|
||||
if (!params.success) {
|
||||
return reply.code(400).send({
|
||||
message: '목표 식별자가 올바르지 않습니다.',
|
||||
})
|
||||
}
|
||||
|
||||
const [existingGoal] = await db
|
||||
.select()
|
||||
.from(goals)
|
||||
.where(and(eq(goals.id, params.data.goalId), eq(goals.userId, user.id)))
|
||||
.limit(1)
|
||||
|
||||
if (!existingGoal) {
|
||||
return reply.code(404).send({
|
||||
message: '목표를 찾을 수 없습니다.',
|
||||
})
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(goals)
|
||||
.where(and(eq(goals.id, params.data.goalId), eq(goals.userId, user.id)))
|
||||
|
||||
return {
|
||||
message: '목표가 삭제되었습니다.',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -67,10 +67,7 @@ export async function registerPlannerRoutes(app) {
|
||||
.orderBy(asc(plannerEntries.entryDate))
|
||||
|
||||
return {
|
||||
entries: entries.map((entry) => ({
|
||||
...entry,
|
||||
payload: JSON.parse(entry.payload),
|
||||
})),
|
||||
entries,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -101,12 +98,7 @@ export async function registerPlannerRoutes(app) {
|
||||
.limit(1)
|
||||
|
||||
return {
|
||||
entry: entry
|
||||
? {
|
||||
...entry,
|
||||
payload: JSON.parse(entry.payload),
|
||||
}
|
||||
: null,
|
||||
entry: entry ?? null,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -141,14 +133,14 @@ export async function registerPlannerRoutes(app) {
|
||||
.values({
|
||||
userId: user.id,
|
||||
entryDate: dateResult.data,
|
||||
payload: JSON.stringify(payloadResult.data.payload),
|
||||
payload: payloadResult.data.payload,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [plannerEntries.userId, plannerEntries.entryDate],
|
||||
set: {
|
||||
payload: JSON.stringify(payloadResult.data.payload),
|
||||
payload: payloadResult.data.payload,
|
||||
updatedAt: now,
|
||||
},
|
||||
})
|
||||
@@ -156,10 +148,7 @@ export async function registerPlannerRoutes(app) {
|
||||
|
||||
return {
|
||||
message: '플래너가 저장되었습니다.',
|
||||
entry: {
|
||||
...entry,
|
||||
payload: JSON.parse(entry.payload),
|
||||
},
|
||||
entry,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Fastify from 'fastify'
|
||||
import cors from '@fastify/cors'
|
||||
import { env } from './config.js'
|
||||
import { sqlite } from './db/client.js'
|
||||
import { pool } from './db/client.js'
|
||||
import { ensureDatabaseSchema } from './db/init.js'
|
||||
import { registerAuthRoutes } from './routes/auth.js'
|
||||
import { registerGoalRoutes } from './routes/goals.js'
|
||||
@@ -11,7 +11,7 @@ const app = Fastify({
|
||||
logger: true,
|
||||
})
|
||||
|
||||
ensureDatabaseSchema()
|
||||
await ensureDatabaseSchema()
|
||||
|
||||
await app.register(cors, {
|
||||
origin: env.CORS_ORIGIN,
|
||||
@@ -23,13 +23,14 @@ await registerGoalRoutes(app)
|
||||
await registerPlannerRoutes(app)
|
||||
|
||||
app.get('/health', async () => {
|
||||
const version = sqlite.prepare('select sqlite_version() as version').get()
|
||||
const versionResult = await pool.query('select version() as version')
|
||||
const version = versionResult.rows[0]
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
service: 'ten-minute-planner-backend',
|
||||
database: {
|
||||
client: 'sqlite',
|
||||
client: 'postgresql',
|
||||
version: version?.version ?? 'unknown',
|
||||
},
|
||||
}
|
||||
@@ -37,11 +38,11 @@ app.get('/health', async () => {
|
||||
|
||||
app.get('/api/meta', async () => ({
|
||||
auth: 'active',
|
||||
storage: 'sqlite',
|
||||
storage: 'postgresql',
|
||||
orm: 'drizzle',
|
||||
notes: [
|
||||
'회원가입, 로그인, 현재 사용자 확인 API가 준비되어 있습니다.',
|
||||
'사용자별 목표 목록과 생성 API가 준비되어 있습니다.',
|
||||
'사용자별 목표 목록, 수정, 삭제 API가 준비되어 있습니다.',
|
||||
'사용자별 플래너 저장 및 조회 API가 준비되어 있습니다.',
|
||||
],
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user