From 18f7dba46a998efe0abc612b5715b892adf286c2 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Tue, 10 Mar 2026 20:52:51 -0600 Subject: [PATCH 01/19] feat(drizzle): add Drizzle ORM schema definitions and infrastructure Add drizzle-orm and drizzle-kit as dependencies. Define typed table schemas for all 7 modules (attendance, content, doing, giving, membership, messaging, reporting). Add connection factory in src/db/drizzle.ts with per-module singleton caching using the existing mysql2 pool from @churchapps/apihelper. Co-Authored-By: Claude Opus 4.6 --- .gitignore | 3 + drizzle.config.ts | 7 + package-lock.json | 2488 +++++++++++++++++++++++++---------- package.json | 8 + src/db/drizzle.ts | 32 + src/db/schema/attendance.ts | 101 ++ src/db/schema/content.ts | 359 +++++ src/db/schema/doing.ts | 183 +++ src/db/schema/giving.ts | 141 ++ src/db/schema/index.ts | 6 + src/db/schema/membership.ts | 396 ++++++ src/db/schema/messaging.ts | 183 +++ 12 files changed, 3210 insertions(+), 697 deletions(-) create mode 100644 drizzle.config.ts create mode 100644 src/db/drizzle.ts create mode 100644 src/db/schema/attendance.ts create mode 100644 src/db/schema/content.ts create mode 100644 src/db/schema/doing.ts create mode 100644 src/db/schema/giving.ts create mode 100644 src/db/schema/index.ts create mode 100644 src/db/schema/membership.ts create mode 100644 src/db/schema/messaging.ts diff --git a/.gitignore b/.gitignore index 0d9fe476..ec881e04 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,9 @@ layers/ *.sqlite3 *.db +# Drizzle ORM migrations (generated by drizzle-kit) +drizzle/ + # Certificates *.pem *.key diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 00000000..4ce189af --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/db/schema", + out: "./drizzle", + dialect: "mysql", +}); diff --git a/package-lock.json b/package-lock.json index 0eaa1a2b..f9bf5e26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "body-parser": "^1.20.3", "cors": "^2.8.5", "dayjs": "^1.11.18", + "drizzle-orm": "^0.45.1", "expo-server-sdk": "^3.15.0", "express": "^4.21.2", "express-fileupload": "^1.5.1", @@ -61,6 +62,7 @@ "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", "copyfiles": "^2.4.1", + "drizzle-kit": "^0.31.9", "eslint": "^9.33.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", @@ -2169,6 +2171,13 @@ "kuler": "^2.0.0" } }, + "node_modules/@drizzle-team/brocli": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", + "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@emnapi/core": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", @@ -2203,27 +2212,22 @@ "tslib": "^2.4.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], + "node_modules/@esbuild-kit/core-utils": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", + "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", + "deprecated": "Merged into tsx: https://tsx.is", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "esbuild": "~0.18.20", + "source-map-support": "^0.5.21" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", "cpu": [ "arm" ], @@ -2234,13 +2238,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", "cpu": [ "arm64" ], @@ -2251,13 +2255,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", "cpu": [ "x64" ], @@ -2268,13 +2272,13 @@ "android" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", "cpu": [ "arm64" ], @@ -2285,13 +2289,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", "cpu": [ "x64" ], @@ -2302,13 +2306,13 @@ "darwin" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", "cpu": [ "arm64" ], @@ -2319,13 +2323,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", "cpu": [ "x64" ], @@ -2336,13 +2340,13 @@ "freebsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", "cpu": [ "arm" ], @@ -2353,13 +2357,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", "cpu": [ "arm64" ], @@ -2370,13 +2374,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", "cpu": [ "ia32" ], @@ -2387,13 +2391,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", "cpu": [ "loong64" ], @@ -2404,13 +2408,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", "cpu": [ "mips64el" ], @@ -2421,13 +2425,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", "cpu": [ "ppc64" ], @@ -2438,13 +2442,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", "cpu": [ "riscv64" ], @@ -2455,13 +2459,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", "cpu": [ "s390x" ], @@ -2472,13 +2476,13 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "cpu": [ "x64" ], @@ -2489,15 +2493,15 @@ "linux" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", @@ -2506,13 +2510,13 @@ "netbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", "cpu": [ "x64" ], @@ -2520,67 +2524,67 @@ "license": "MIT", "optional": true, "os": [ - "netbsd" + "openbsd" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "sunos" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openbsd" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "openharmony" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", "cpu": [ "x64" ], @@ -2588,84 +2592,536 @@ "license": "MIT", "optional": true, "os": [ - "sunos" + "win32" ], "engines": { - "node": ">=18" + "node": ">=12" } }, - "node_modules/@esbuild/win32-arm64": { + "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/@esbuild-kit/core-utils/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/@esbuild-kit/esm-loader": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", + "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", + "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, + "license": "MIT", + "dependencies": { + "@esbuild-kit/core-utils": "^3.3.2", + "get-tsconfig": "^4.7.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "aix" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-ia32": { + "node_modules/@esbuild/android-arm": { "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ - "ia32" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@esbuild/win32-x64": { + "node_modules/@esbuild/android-arm64": { "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "android" ], "engines": { "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/config-array": { @@ -6889,971 +7345,1596 @@ "node": ">=0.10" } }, - "node_modules/cli-cursor": { - "version": "3.1.0", + "node_modules/cli-cursor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress-footer": { + "version": "2.3.3", + "dev": true, + "license": "ISC", + "dependencies": { + "cli-color": "^2.0.4", + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "mute-stream": "0.0.8", + "process-utils": "^4.0.0", + "timers-ext": "^0.1.7", + "type": "^2.7.2" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-sprintf-format": { + "version": "1.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cli-color": "^2.0.1", + "es5-ext": "^0.10.53", + "sprintf-kit": "^2.0.1", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-sprintf-format/node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-sprintf-format/node_modules/supports-color": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/co": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "5.0.3", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compress-commons": { + "version": "4.1.2", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^3.1.0" + "buffer-crc32": "^0.2.13", + "crc32-stream": "^4.0.2", + "normalize-path": "^3.0.0", + "readable-stream": "^3.6.0" }, "engines": { - "node": ">=8" + "node": ">= 10" } }, - "node_modules/cli-progress-footer": { - "version": "2.3.3", + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "3.6.2", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "cli-color": "^2.0.4", - "d": "^1.0.1", - "es5-ext": "^0.10.64", - "mute-stream": "0.0.8", - "process-utils": "^4.0.0", - "timers-ext": "^0.1.7", - "type": "^2.7.2" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": ">=10.0" + "node": ">= 6" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", + "node_modules/compress-commons/node_modules/string_decoder": { + "version": "1.3.0", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "dependencies": { + "safe-buffer": "~5.2.0" } }, - "node_modules/cli-sprintf-format": { - "version": "1.1.1", + "node_modules/concat-map": { + "version": "0.0.1", "dev": true, - "license": "ISC", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "license": "MIT", "dependencies": { - "cli-color": "^2.0.1", - "es5-ext": "^0.10.53", - "sprintf-kit": "^2.0.1", - "supports-color": "^6.1.0" + "safe-buffer": "5.2.1" }, "engines": { - "node": ">=6.0" + "node": ">= 0.6" } }, - "node_modules/cli-sprintf-format/node_modules/has-flag": { - "version": "3.0.0", + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.6" } }, - "node_modules/cli-sprintf-format/node_modules/supports-color": { - "version": "6.1.0", + "node_modules/cookie-signature": { + "version": "1.0.7", + "license": "MIT" + }, + "node_modules/cookiejar": { + "version": "2.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/copyfiles": { + "version": "2.4.1", "dev": true, "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "glob": "^7.0.5", + "minimatch": "^3.0.3", + "mkdirp": "^1.0.4", + "noms": "0.0.0", + "through2": "^2.0.1", + "untildify": "^4.0.0", + "yargs": "^16.1.0" }, - "engines": { - "node": ">=6" + "bin": { + "copyfiles": "copyfiles", + "copyup": "copyfiles" } }, - "node_modules/cli-width": { - "version": "3.0.0", + "node_modules/copyfiles/node_modules/brace-expansion": { + "version": "1.1.12", "dev": true, - "license": "ISC", - "engines": { - "node": ">= 10" + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/cliui": { - "version": "7.0.4", + "node_modules/copyfiles/node_modules/minimatch": { + "version": "3.1.2", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", + "node_modules/core-util-is": { + "version": "1.0.3", "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "object-assign": "^4", + "vary": "^1" }, "engines": { - "node": ">=10" + "node": ">= 0.10" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/clone": { - "version": "1.0.4", + "node_modules/crc-32": { + "version": "1.2.2", "dev": true, - "license": "MIT", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, "engines": { "node": ">=0.8" } }, - "node_modules/clone-response": { - "version": "1.0.3", + "node_modules/crc32-stream": { + "version": "4.0.3", "dev": true, "license": "MIT", "dependencies": { - "mimic-response": "^1.0.0" + "crc-32": "^1.2.0", + "readable-stream": "^3.4.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 10" } }, - "node_modules/co": { - "version": "4.6.0", + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "3.6.2", "dev": true, "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" + "node": ">= 6" } }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", + "node_modules/crc32-stream/node_modules/string_decoder": { + "version": "1.3.0", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } }, - "node_modules/color": { - "version": "5.0.3", + "node_modules/create-jest": { + "version": "29.7.0", + "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" }, "engines": { - "node": ">=18" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/color-convert": { - "version": "2.0.1", + "node_modules/cron-parser": { + "version": "4.9.0", + "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "luxon": "^3.2.1" }, "engines": { - "node": ">=7.0.0" + "node": ">=12.0.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "license": "MIT" - }, - "node_modules/color-string": { - "version": "2.1.4", + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, "license": "MIT", "dependencies": { - "color-name": "^2.0.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=18" + "node": ">= 8" } }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.0", - "license": "MIT", + "node_modules/d": { + "version": "1.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, "engines": { - "node": ">=12.20" + "node": ">=0.12" } }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", + "node_modules/data-view-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "dev": true, "license": "MIT", "dependencies": { - "color-name": "^2.0.0" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" }, "engines": { - "node": ">=14.6" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" } }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.0", + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "dev": true, "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, "engines": { - "node": ">=12.20" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/combined-stream": { - "version": "1.0.8", + "node_modules/dayjs": { + "version": "1.11.19", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, "license": "MIT", "dependencies": { - "delayed-stream": "~1.0.0" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.8" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/commander": { - "version": "4.1.1", + "node_modules/decompress": { + "version": "4.2.1", "dev": true, "license": "MIT", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, "engines": { - "node": ">= 6" + "node": ">=4" } }, - "node_modules/component-emitter": { - "version": "1.3.1", + "node_modules/decompress-response": { + "version": "6.0.0", "dev": true, "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/compress-commons": { - "version": "4.1.2", + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", "dev": true, "license": "MIT", - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, "engines": { - "node": ">= 10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/compress-commons/node_modules/readable-stream": { - "version": "3.6.2", + "node_modules/decompress-tar": { + "version": "4.1.1", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" }, "engines": { - "node": ">= 6" + "node": ">=4" } }, - "node_modules/compress-commons/node_modules/string_decoder": { - "version": "1.3.0", + "node_modules/decompress-tar/node_modules/bl": { + "version": "1.2.3", "dev": true, "license": "MIT", "dependencies": { - "safe-buffer": "~5.2.0" + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, - "node_modules/concat-map": { - "version": "0.0.1", + "node_modules/decompress-tar/node_modules/file-type": { + "version": "5.2.0", "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "0.5.4", "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=4" } }, - "node_modules/content-type": { - "version": "1.0.5", + "node_modules/decompress-tar/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=0.10.0" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", + "node_modules/decompress-tar/node_modules/isarray": { + "version": "1.0.0", "dev": true, "license": "MIT" }, - "node_modules/cookie": { - "version": "0.7.2", + "node_modules/decompress-tar/node_modules/readable-stream": { + "version": "2.3.8", + "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "license": "MIT" - }, - "node_modules/cookiejar": { - "version": "2.1.4", + "node_modules/decompress-tar/node_modules/safe-buffer": { + "version": "5.1.2", "dev": true, "license": "MIT" }, - "node_modules/copyfiles": { - "version": "2.4.1", + "node_modules/decompress-tar/node_modules/string_decoder": { + "version": "1.1.1", "dev": true, "license": "MIT", "dependencies": { - "glob": "^7.0.5", - "minimatch": "^3.0.3", - "mkdirp": "^1.0.4", - "noms": "0.0.0", - "through2": "^2.0.1", - "untildify": "^4.0.0", - "yargs": "^16.1.0" - }, - "bin": { - "copyfiles": "copyfiles", - "copyup": "copyfiles" + "safe-buffer": "~5.1.0" } }, - "node_modules/copyfiles/node_modules/brace-expansion": { - "version": "1.1.12", + "node_modules/decompress-tar/node_modules/tar-stream": { + "version": "1.6.2", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/copyfiles/node_modules/minimatch": { - "version": "3.1.2", + "node_modules/decompress-tarbz2": { + "version": "4.1.1", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" }, "engines": { - "node": "*" + "node": ">=4" } }, - "node_modules/core-util-is": { - "version": "1.0.3", + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "node_modules/cors": { - "version": "2.8.6", + "node_modules/decompress-tarbz2/node_modules/is-stream": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "dev": true, "license": "MIT", "dependencies": { - "object-assign": "^4", - "vary": "^1" + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" }, "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=4" } }, - "node_modules/crc-32": { - "version": "1.2.2", + "node_modules/decompress-targz/node_modules/file-type": { + "version": "5.2.0", "dev": true, - "license": "Apache-2.0", - "bin": { - "crc32": "bin/crc32.njs" - }, + "license": "MIT", "engines": { - "node": ">=0.8" + "node": ">=4" } }, - "node_modules/crc32-stream": { - "version": "4.0.3", + "node_modules/decompress-targz/node_modules/is-stream": { + "version": "1.1.0", "dev": true, "license": "MIT", - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, "engines": { - "node": ">= 10" + "node": ">=0.10.0" } }, - "node_modules/crc32-stream/node_modules/readable-stream": { - "version": "3.6.2", + "node_modules/decompress-unzip": { + "version": "4.0.1", "dev": true, "license": "MIT", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" }, "engines": { - "node": ">= 6" + "node": ">=4" } }, - "node_modules/crc32-stream/node_modules/string_decoder": { - "version": "1.3.0", + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/create-jest": { - "version": "29.7.0", + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "prompts": "^2.0.1" - }, - "bin": { - "create-jest": "bin/create-jest.js" + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=0.10.0" } }, - "node_modules/cron-parser": { - "version": "4.9.0", + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", "dev": true, "license": "MIT", "dependencies": { - "luxon": "^3.2.1" + "pify": "^3.0.0" }, "engines": { - "node": ">=12.0.0" + "node": ">=4" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, "engines": { - "node": ">= 8" + "node": ">=4" } }, - "node_modules/d": { - "version": "1.0.2", + "node_modules/dedent": { + "version": "1.7.1", "dev": true, - "license": "ISC", - "dependencies": { - "es5-ext": "^0.10.64", - "type": "^2.7.2" + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" }, - "engines": { - "node": ">=0.12" + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", + "node_modules/defaults": { + "version": "1.0.4", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" + "clone": "^1.0.2" }, "funding": { - "url": "https://github.com/sponsors/inspect-js" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", + "node_modules/defer-to-connect": { + "version": "2.0.1", "dev": true, "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=10" } }, - "node_modules/dayjs": { - "version": "1.11.19", - "license": "MIT" + "node_modules/deferred": { + "version": "0.7.11", + "dev": true, + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.50", + "event-emitter": "^0.3.5", + "next-tick": "^1.0.0", + "timers-ext": "^0.1.7" + } }, - "node_modules/debug": { - "version": "4.4.3", + "node_modules/define-data-property": { + "version": "1.1.4", "dev": true, "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { - "node": ">=6.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/decompress": { - "version": "4.2.1", + "node_modules/define-lazy-prop": { + "version": "2.0.0", "dev": true, "license": "MIT", - "dependencies": { - "decompress-tar": "^4.0.0", - "decompress-tarbz2": "^4.0.0", - "decompress-targz": "^4.0.0", - "decompress-unzip": "^4.0.1", - "graceful-fs": "^4.1.10", - "make-dir": "^1.0.0", - "pify": "^2.3.0", - "strip-dirs": "^2.0.0" - }, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/decompress-response": { - "version": "6.0.0", + "node_modules/define-properties": { + "version": "1.2.1", "dev": true, "license": "MIT", "dependencies": { - "mimic-response": "^3.1.0" + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "dev": true, + "node_modules/delayed-stream": { + "version": "1.0.0", "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.4.0" } }, - "node_modules/decompress-tar": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "file-type": "^5.2.0", - "is-stream": "^1.1.0", - "tar-stream": "^1.5.2" - }, + "node_modules/denque": { + "version": "2.1.0", + "license": "Apache-2.0", "engines": { - "node": ">=4" + "node": ">=0.10" } }, - "node_modules/decompress-tar/node_modules/bl": { - "version": "1.2.3", - "dev": true, + "node_modules/depd": { + "version": "2.0.0", "license": "MIT", - "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "engines": { + "node": ">= 0.8" } }, - "node_modules/decompress-tar/node_modules/file-type": { - "version": "5.2.0", + "node_modules/desm": { + "version": "1.3.1", "dev": true, + "license": "MIT" + }, + "node_modules/destroy": { + "version": "1.2.0", "license": "MIT", "engines": { - "node": ">=4" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/decompress-tar/node_modules/is-stream": { - "version": "1.1.0", + "node_modules/detect-newline": { + "version": "3.1.0", "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/decompress-tar/node_modules/isarray": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/decompress-tar/node_modules/readable-stream": { - "version": "2.3.8", + "node_modules/dezalgo": { + "version": "1.0.4", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "asap": "^2.0.0", + "wrappy": "1" } }, - "node_modules/decompress-tar/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/decompress-tar/node_modules/string_decoder": { - "version": "1.1.1", + "node_modules/diff-sequences": { + "version": "29.6.3", "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/decompress-tar/node_modules/tar-stream": { - "version": "1.6.2", + "node_modules/dir-glob": { + "version": "3.0.1", "dev": true, "license": "MIT", "dependencies": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" + "path-type": "^4.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=8" } }, - "node_modules/decompress-tarbz2": { - "version": "4.1.1", + "node_modules/doctrine": { + "version": "2.1.0", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" + "esutils": "^2.0.2" }, "engines": { - "node": ">=4" + "node": ">=0.10.0" } }, - "node_modules/decompress-tarbz2/node_modules/file-type": { - "version": "6.2.0", - "dev": true, - "license": "MIT", + "node_modules/dotenv": { + "version": "17.2.3", + "license": "BSD-2-Clause", "engines": { - "node": ">=4" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "node_modules/decompress-tarbz2/node_modules/is-stream": { - "version": "1.1.0", + "node_modules/dotenv-expand": { + "version": "10.0.0", "dev": true, - "license": "MIT", + "license": "BSD-2-Clause", "engines": { - "node": ">=0.10.0" + "node": ">=12" } }, - "node_modules/decompress-targz": { - "version": "4.1.1", + "node_modules/drizzle-kit": { + "version": "0.31.9", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.9.tgz", + "integrity": "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==", "dev": true, "license": "MIT", "dependencies": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" + "@drizzle-team/brocli": "^0.10.2", + "@esbuild-kit/esm-loader": "^2.5.5", + "esbuild": "^0.25.4", + "esbuild-register": "^3.5.0" }, - "engines": { - "node": ">=4" + "bin": { + "drizzle-kit": "bin.cjs" } }, - "node_modules/decompress-targz/node_modules/file-type": { - "version": "5.2.0", + "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/decompress-targz/node_modules/is-stream": { - "version": "1.1.0", + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/decompress-unzip": { - "version": "4.0.1", + "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/decompress-unzip/node_modules/file-type": { - "version": "3.9.0", + "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/decompress-unzip/node_modules/get-stream": { - "version": "2.3.1", + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/decompress/node_modules/make-dir": { - "version": "1.3.0", + "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/decompress/node_modules/make-dir/node_modules/pify": { - "version": "3.0.0", + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=4" + "node": ">=18" } }, - "node_modules/dedent": { - "version": "1.7.1", + "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/deep-is": { - "version": "0.1.4", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/deepmerge": { - "version": "4.3.1", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/defaults": { - "version": "1.0.4", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=18" } }, - "node_modules/deferred": { - "version": "0.7.11", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "ISC", - "dependencies": { - "d": "^1.0.1", - "es5-ext": "^0.10.50", - "event-emitter": "^0.3.5", - "next-tick": "^1.0.0", - "timers-ext": "^0.1.7" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/define-data-property": { - "version": "1.1.4", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/define-properties": { - "version": "1.2.1", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=18" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", + "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=0.4.0" + "node": ">=18" } }, - "node_modules/denque": { - "version": "2.1.0", - "license": "Apache-2.0", + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=0.10" + "node": ">=18" } }, - "node_modules/depd": { - "version": "2.0.0", + "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">= 0.8" + "node": ">=18" } }, - "node_modules/desm": { - "version": "1.3.1", + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/destroy": { - "version": "1.2.0", "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" } }, - "node_modules/detect-newline": { - "version": "3.1.0", + "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/dezalgo": { - "version": "1.0.4", + "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC", - "dependencies": { - "asap": "^2.0.0", - "wrappy": "1" + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", + "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/dir-glob": { - "version": "3.0.1", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/doctrine": { - "version": "2.1.0", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/dotenv": { - "version": "17.2.3", - "license": "BSD-2-Clause", + "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" + "node": ">=18" } }, - "node_modules/dotenv-expand": { - "version": "10.0.0", + "node_modules/drizzle-kit/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, - "license": "BSD-2-Clause", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=12" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/drizzle-orm": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz", + "integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==", + "license": "Apache-2.0", + "peerDependencies": { + "@aws-sdk/client-rds-data": ">=3", + "@cloudflare/workers-types": ">=4", + "@electric-sql/pglite": ">=0.2.0", + "@libsql/client": ">=0.10.0", + "@libsql/client-wasm": ">=0.10.0", + "@neondatabase/serverless": ">=0.10.0", + "@op-engineering/op-sqlite": ">=2", + "@opentelemetry/api": "^1.4.1", + "@planetscale/database": ">=1.13", + "@prisma/client": "*", + "@tidbcloud/serverless": "*", + "@types/better-sqlite3": "*", + "@types/pg": "*", + "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", + "@vercel/postgres": ">=0.8.0", + "@xata.io/client": "*", + "better-sqlite3": ">=7", + "bun-types": "*", + "expo-sqlite": ">=14.0.0", + "gel": ">=2", + "knex": "*", + "kysely": "*", + "mysql2": ">=2", + "pg": ">=8", + "postgres": ">=3", + "sql.js": ">=1", + "sqlite3": ">=5" + }, + "peerDependenciesMeta": { + "@aws-sdk/client-rds-data": { + "optional": true + }, + "@cloudflare/workers-types": { + "optional": true + }, + "@electric-sql/pglite": { + "optional": true + }, + "@libsql/client": { + "optional": true + }, + "@libsql/client-wasm": { + "optional": true + }, + "@neondatabase/serverless": { + "optional": true + }, + "@op-engineering/op-sqlite": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@tidbcloud/serverless": { + "optional": true + }, + "@types/better-sqlite3": { + "optional": true + }, + "@types/pg": { + "optional": true + }, + "@types/sql.js": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/postgres": { + "optional": true + }, + "@xata.io/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "bun-types": { + "optional": true + }, + "expo-sqlite": { + "optional": true + }, + "gel": { + "optional": true + }, + "knex": { + "optional": true + }, + "kysely": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "postgres": { + "optional": true + }, + "prisma": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + } } }, "node_modules/dunder-proto": { @@ -8184,6 +9265,19 @@ "@esbuild/win32-x64": "0.27.2" } }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escalade": { "version": "3.2.0", "dev": true, diff --git a/package.json b/package.json index 2e00d66b..b771039e 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,12 @@ "deploy-prod": "npm run build:prod && serverless deploy --stage prod --alias current", "test": "jest --coverage", "test:watch": "jest --watch", + "test:integration": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.integration.config.cjs", + "test:integration:attendance": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.integration.config.cjs --testPathPattern=attendance", + "test:integration:giving": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.integration.config.cjs --testPathPattern=giving", + "test:integration:content": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.integration.config.cjs --testPathPattern=content", + "test:integration:messaging": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.integration.config.cjs --testPathPattern=messaging", + "test:integration:membership": "NODE_OPTIONS='--experimental-vm-modules' jest --config jest.integration.config.cjs --testPathPattern=membership", "migrate-env": "node tools/migrate-env.js", "scheduled-tasks": "tsx tools/run-scheduled-tasks.ts" }, @@ -69,6 +75,7 @@ "body-parser": "^1.20.3", "cors": "^2.8.5", "dayjs": "^1.11.18", + "drizzle-orm": "^0.45.1", "expo-server-sdk": "^3.15.0", "express": "^4.21.2", "express-fileupload": "^1.5.1", @@ -103,6 +110,7 @@ "@typescript-eslint/eslint-plugin": "^8.39.1", "@typescript-eslint/parser": "^8.39.1", "copyfiles": "^2.4.1", + "drizzle-kit": "^0.31.9", "eslint": "^9.33.0", "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", diff --git a/src/db/drizzle.ts b/src/db/drizzle.ts new file mode 100644 index 00000000..3ed2d9db --- /dev/null +++ b/src/db/drizzle.ts @@ -0,0 +1,32 @@ +import { drizzle } from "drizzle-orm/mysql2"; +import type { MySql2Database } from "drizzle-orm/mysql2"; +import { MultiDatabasePool } from "../shared/infrastructure/MultiDatabasePool.js"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const instances = new Map>(); + +/** + * Get a Drizzle ORM instance for a module's database. + * Reuses the existing mysql2 pools from MultiDatabasePool — zero extra connections. + * + * Note: boolean columns use Drizzle's boolean() which maps to tinyint(1). + * The actual DB uses BIT(1), but MultiDatabasePool's typeCast converts BIT(1) + * to JS booleans at the driver level, so Drizzle reads correct values. + * For schema generation/migration, use drizzle-kit introspect to verify. + */ +export function getDrizzleDb(moduleName: string): MySql2Database { + let db = instances.get(moduleName); + if (!db) { + const pool = MultiDatabasePool.getPool(moduleName); + db = drizzle(pool); + instances.set(moduleName, db); + } + return db; +} + +/** + * Clear cached Drizzle instances (useful for tests or pool reset). + */ +export function clearDrizzleInstances() { + instances.clear(); +} diff --git a/src/db/schema/attendance.ts b/src/db/schema/attendance.ts new file mode 100644 index 00000000..8b93f718 --- /dev/null +++ b/src/db/schema/attendance.ts @@ -0,0 +1,101 @@ +import { mysqlTable, char, varchar, datetime, boolean, index } from "drizzle-orm/mysql-core"; + +export const campuses = mysqlTable("campuses", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + name: varchar("name", { length: 255 }), + address1: varchar("address1", { length: 50 }), + address2: varchar("address2", { length: 50 }), + city: varchar("city", { length: 50 }), + state: varchar("state", { length: 10 }), + zip: varchar("zip", { length: 10 }), + removed: boolean("removed"), +}, (t) => [ + index("churchId").on(t.churchId), +]); + +export const services = mysqlTable("services", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + campusId: char("campusId", { length: 11 }), + name: varchar("name", { length: 50 }), + removed: boolean("removed"), +}, (t) => [ + index("churchId").on(t.churchId), + index("campusId").on(t.campusId), +]); + +export const serviceTimes = mysqlTable("serviceTimes", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + serviceId: char("serviceId", { length: 11 }), + name: varchar("name", { length: 50 }), + removed: boolean("removed"), +}, (t) => [ + index("churchId").on(t.churchId), + index("serviceId").on(t.serviceId), + index("idx_church_service_removed").on(t.churchId, t.serviceId, t.removed), +]); + +export const sessions = mysqlTable("sessions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + groupId: char("groupId", { length: 11 }), + serviceTimeId: char("serviceTimeId", { length: 11 }), + sessionDate: datetime("sessionDate"), +}, (t) => [ + index("churchId").on(t.churchId), + index("groupId").on(t.groupId), + index("serviceTimeId").on(t.serviceTimeId), + index("idx_church_session_date").on(t.churchId, t.sessionDate), + index("idx_church_group_service").on(t.churchId, t.groupId, t.serviceTimeId), +]); + +export const visits = mysqlTable("visits", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + serviceId: char("serviceId", { length: 11 }), + groupId: char("groupId", { length: 11 }), + visitDate: datetime("visitDate"), + checkinTime: datetime("checkinTime"), + addedBy: char("addedBy", { length: 11 }), +}, (t) => [ + index("churchId").on(t.churchId), + index("personId").on(t.personId), + index("serviceId").on(t.serviceId), + index("groupId").on(t.groupId), + index("idx_church_visit_date").on(t.churchId, t.visitDate), + index("idx_church_person").on(t.churchId, t.personId), +]); + +export const visitSessions = mysqlTable("visitSessions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + visitId: char("visitId", { length: 11 }), + sessionId: char("sessionId", { length: 11 }), +}, (t) => [ + index("churchId").on(t.churchId), + index("visitId").on(t.visitId), + index("sessionId").on(t.sessionId), +]); + +export const groupServiceTimes = mysqlTable("groupServiceTimes", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + groupId: char("groupId", { length: 11 }), + serviceTimeId: char("serviceTimeId", { length: 11 }), +}, (t) => [ + index("churchId").on(t.churchId), + index("groupId").on(t.groupId), + index("serviceTimeId").on(t.serviceTimeId), +]); + +export const attendanceSettings = mysqlTable("settings", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + keyName: varchar("keyName", { length: 255 }), + value: varchar("value", { length: 255 }), +}, (t) => [ + index("churchId").on(t.churchId), +]); diff --git a/src/db/schema/content.ts b/src/db/schema/content.ts new file mode 100644 index 00000000..37634190 --- /dev/null +++ b/src/db/schema/content.ts @@ -0,0 +1,359 @@ +import { mysqlTable, char, varchar, datetime, date, boolean, int, float, text, mediumtext, longtext, index, uniqueIndex } from "drizzle-orm/mysql-core"; + +export const arrangementKeys = mysqlTable("arrangementKeys", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + arrangementId: char("arrangementId", { length: 11 }), + keySignature: varchar("keySignature", { length: 10 }), + shortDescription: varchar("shortDescription", { length: 45 }), +}); + +export const arrangements = mysqlTable("arrangements", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + songId: char("songId", { length: 11 }), + songDetailId: char("songDetailId", { length: 11 }), + name: varchar("name", { length: 45 }), + lyrics: text("lyrics"), + freeShowId: varchar("freeShowId", { length: 45 }), +}, (t) => [ + index("ix_churchId_songId").on(t.churchId, t.songId), +]); + +export const bibleBooks = mysqlTable("bibleBooks", { + id: char("id", { length: 11 }).notNull().primaryKey(), + translationKey: varchar("translationKey", { length: 45 }), + keyName: varchar("keyName", { length: 45 }), + abbreviation: varchar("abbreviation", { length: 45 }), + name: varchar("name", { length: 45 }), + sort: int("sort"), +}, (t) => [ + index("ix_translationKey").on(t.translationKey), +]); + +export const bibleChapters = mysqlTable("bibleChapters", { + id: char("id", { length: 11 }).notNull().primaryKey(), + translationKey: varchar("translationKey", { length: 45 }), + bookKey: varchar("bookKey", { length: 45 }), + keyName: varchar("keyName", { length: 45 }), + number: int("number"), +}, (t) => [ + index("ix_translationKey_bookKey").on(t.translationKey, t.bookKey), +]); + +export const bibleLookups = mysqlTable("bibleLookups", { + id: char("id", { length: 11 }).notNull().primaryKey(), + translationKey: varchar("translationKey", { length: 45 }), + lookupTime: datetime("lookupTime"), + ipAddress: varchar("ipAddress", { length: 45 }), + startVerseKey: varchar("startVerseKey", { length: 15 }), + endVerseKey: varchar("endVerseKey", { length: 15 }), +}); + +export const bibleTranslations = mysqlTable("bibleTranslations", { + id: char("id", { length: 11 }).notNull().primaryKey(), + abbreviation: varchar("abbreviation", { length: 10 }), + name: varchar("name", { length: 255 }), + nameLocal: varchar("nameLocal", { length: 255 }), + description: varchar("description", { length: 1000 }), + source: varchar("source", { length: 45 }), + sourceKey: varchar("sourceKey", { length: 45 }), + language: varchar("language", { length: 45 }), + countries: varchar("countries", { length: 255 }), + copyright: varchar("copyright", { length: 1000 }), + attributionRequired: boolean("attributionRequired"), + attributionString: varchar("attributionString", { length: 1000 }), +}); + +export const bibleVerses = mysqlTable("bibleVerses", { + id: char("id", { length: 11 }).notNull().primaryKey(), + translationKey: varchar("translationKey", { length: 45 }), + chapterKey: varchar("chapterKey", { length: 45 }), + keyName: varchar("keyName", { length: 45 }), + number: int("number"), +}, (t) => [ + index("ix_translationKey_chapterKey").on(t.translationKey, t.chapterKey), +]); + +export const bibleVerseTexts = mysqlTable("bibleVerseTexts", { + id: char("id", { length: 11 }).notNull().primaryKey(), + translationKey: varchar("translationKey", { length: 45 }), + verseKey: varchar("verseKey", { length: 45 }), + bookKey: varchar("bookKey", { length: 45 }), + chapterNumber: int("chapterNumber"), + verseNumber: int("verseNumber"), + content: varchar("content", { length: 1000 }), + newParagraph: boolean("newParagraph"), +}, (t) => [ + uniqueIndex("uq_translationKey_verseKey").on(t.translationKey, t.verseKey), +]); + +export const blocks = mysqlTable("blocks", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + blockType: varchar("blockType", { length: 45 }), + name: varchar("name", { length: 45 }), +}); + +export const curatedCalendars = mysqlTable("curatedCalendars", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + name: varchar("name", { length: 45 }), +}); + +export const curatedEvents = mysqlTable("curatedEvents", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + curatedCalendarId: char("curatedCalendarId", { length: 11 }), + groupId: char("groupId", { length: 11 }), + eventId: char("eventId", { length: 11 }), +}, (t) => [ + index("ix_churchId_curatedCalendarId").on(t.churchId, t.curatedCalendarId), +]); + +export const elements = mysqlTable("elements", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + sectionId: char("sectionId", { length: 11 }), + blockId: char("blockId", { length: 11 }), + elementType: varchar("elementType", { length: 45 }), + sort: float("sort"), + parentId: char("parentId", { length: 11 }), + answersJSON: mediumtext("answersJSON"), + stylesJSON: mediumtext("stylesJSON"), + animationsJSON: mediumtext("animationsJSON"), +}, (t) => [ + index("ix_churchId_blockId_sort").on(t.churchId, t.blockId, t.sort), +]); + +export const eventExceptions = mysqlTable("eventExceptions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + eventId: char("eventId", { length: 11 }), + exceptionDate: datetime("exceptionDate"), + recurrenceDate: datetime("recurrenceDate"), +}); + +export const events = mysqlTable("events", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + groupId: char("groupId", { length: 11 }), + allDay: boolean("allDay"), + start: datetime("start"), + end: datetime("end"), + title: varchar("title", { length: 255 }), + description: mediumtext("description"), + visibility: varchar("visibility", { length: 45 }), + recurrenceRule: varchar("recurrenceRule", { length: 255 }), + registrationEnabled: boolean("registrationEnabled"), + capacity: int("capacity"), + registrationOpenDate: datetime("registrationOpenDate"), + registrationCloseDate: datetime("registrationCloseDate"), + tags: varchar("tags", { length: 500 }), + formId: char("formId", { length: 11 }), +}, (t) => [ + index("ix_churchId_groupId").on(t.churchId, t.groupId), +]); + +export const files = mysqlTable("files", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + contentType: varchar("contentType", { length: 45 }), + contentId: char("contentId", { length: 11 }), + fileName: varchar("fileName", { length: 255 }), + contentPath: varchar("contentPath", { length: 1024 }), + fileType: varchar("fileType", { length: 45 }), + size: int("size"), + dateModified: datetime("dateModified"), +}, (t) => [ + index("ix_churchId_id").on(t.churchId, t.id), +]); + +export const globalStyles = mysqlTable("globalStyles", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + fonts: text("fonts"), + palette: text("palette"), + typography: text("typography"), + spacing: text("spacing"), + borderRadius: text("borderRadius"), + customCss: text("customCss"), + customJS: text("customJS"), +}); + +export const links = mysqlTable("links", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + category: varchar("category", { length: 45 }), + url: varchar("url", { length: 255 }), + linkType: varchar("linkType", { length: 45 }), + linkData: varchar("linkData", { length: 255 }), + icon: varchar("icon", { length: 45 }), + text: varchar("text", { length: 255 }), + sort: float("sort"), + photo: varchar("photo", { length: 255 }), + parentId: char("parentId", { length: 11 }), + visibility: varchar("visibility", { length: 45 }).default("everyone"), + groupIds: text("groupIds"), +}, (t) => [ + index("churchId").on(t.churchId), +]); + +export const pageHistory = mysqlTable("pageHistory", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + pageId: char("pageId", { length: 11 }), + blockId: char("blockId", { length: 11 }), + snapshotJSON: longtext("snapshotJSON"), + description: varchar("description", { length: 200 }), + userId: char("userId", { length: 11 }), + createdDate: datetime("createdDate"), +}, (t) => [ + index("ix_pageId").on(t.pageId, t.createdDate), + index("ix_blockId").on(t.blockId, t.createdDate), +]); + +export const pages = mysqlTable("pages", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + url: varchar("url", { length: 255 }), + title: varchar("title", { length: 255 }), + layout: varchar("layout", { length: 45 }), +}, (t) => [ + uniqueIndex("uq_churchId_url").on(t.churchId, t.url), +]); + +export const playlists = mysqlTable("playlists", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + title: varchar("title", { length: 255 }), + description: text("description"), + publishDate: datetime("publishDate"), + thumbnail: varchar("thumbnail", { length: 1024 }), +}); + +export const registrationMembers = mysqlTable("registrationMembers", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }).notNull(), + registrationId: char("registrationId", { length: 11 }).notNull(), + personId: char("personId", { length: 11 }), + firstName: varchar("firstName", { length: 100 }), + lastName: varchar("lastName", { length: 100 }), +}, (t) => [ + index("ix_regMembers_registrationId").on(t.registrationId), + index("ix_regMembers_personId").on(t.personId), +]); + +export const registrations = mysqlTable("registrations", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }).notNull(), + eventId: char("eventId", { length: 11 }).notNull(), + personId: char("personId", { length: 11 }), + householdId: char("householdId", { length: 11 }), + status: varchar("status", { length: 20 }).default("pending"), + formSubmissionId: char("formSubmissionId", { length: 11 }), + notes: mediumtext("notes"), + registeredDate: datetime("registeredDate"), + cancelledDate: datetime("cancelledDate"), +}, (t) => [ + index("ix_registrations_churchId_eventId").on(t.churchId, t.eventId), + index("ix_registrations_personId").on(t.personId), + index("ix_registrations_householdId").on(t.householdId), +]); + +export const sections = mysqlTable("sections", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + pageId: char("pageId", { length: 11 }), + blockId: char("blockId", { length: 11 }), + zone: varchar("zone", { length: 45 }), + background: varchar("background", { length: 255 }), + textColor: varchar("textColor", { length: 45 }), + headingColor: varchar("headingColor", { length: 45 }), + linkColor: varchar("linkColor", { length: 45 }), + sort: float("sort"), + targetBlockId: char("targetBlockId", { length: 11 }), + answersJSON: mediumtext("answersJSON"), + stylesJSON: mediumtext("stylesJSON"), + animationsJSON: mediumtext("animationsJSON"), +}, (t) => [ + index("ix_churchId_pageId_sort").on(t.churchId, t.pageId, t.sort), + index("ix_churchId_blockId_sort").on(t.churchId, t.blockId, t.sort), +]); + +export const sermons = mysqlTable("sermons", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + playlistId: char("playlistId", { length: 11 }), + videoType: varchar("videoType", { length: 45 }), + videoData: varchar("videoData", { length: 255 }), + videoUrl: varchar("videoUrl", { length: 1024 }), + title: varchar("title", { length: 255 }), + description: text("description"), + publishDate: datetime("publishDate"), + thumbnail: varchar("thumbnail", { length: 1024 }), + duration: int("duration"), + permanentUrl: boolean("permanentUrl"), +}); + +export const contentSettings = mysqlTable("settings", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + userId: char("userId", { length: 11 }), + keyName: varchar("keyName", { length: 255 }), + value: mediumtext("value"), + public: boolean("public"), +}, (t) => [ + index("churchId").on(t.churchId), + index("ix_churchId_keyName_userId").on(t.churchId, t.keyName, t.userId), +]); + +export const songDetailLinks = mysqlTable("songDetailLinks", { + id: char("id", { length: 11 }).notNull().primaryKey(), + songDetailId: char("songDetailId", { length: 11 }), + service: varchar("service", { length: 45 }), + serviceKey: varchar("serviceKey", { length: 255 }), + url: varchar("url", { length: 255 }), +}); + +export const songDetails = mysqlTable("songDetails", { + id: char("id", { length: 11 }).notNull().primaryKey(), + praiseChartsId: varchar("praiseChartsId", { length: 45 }), + musicBrainzId: varchar("musicBrainzId", { length: 45 }), + title: varchar("title", { length: 45 }), + artist: varchar("artist", { length: 45 }), + album: varchar("album", { length: 45 }), + language: varchar("language", { length: 5 }), + thumbnail: varchar("thumbnail", { length: 255 }), + releaseDate: date("releaseDate"), + bpm: int("bpm"), + keySignature: varchar("keySignature", { length: 5 }), + seconds: int("seconds"), + meter: varchar("meter", { length: 10 }), + tones: varchar("tones", { length: 45 }), +}); + +export const songs = mysqlTable("songs", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + name: varchar("name", { length: 45 }), + dateAdded: date("dateAdded"), +}, (t) => [ + index("ix_churchId_name").on(t.churchId, t.name), +]); + +export const streamingServices = mysqlTable("streamingServices", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + serviceTime: datetime("serviceTime"), + earlyStart: int("earlyStart"), + chatBefore: int("chatBefore"), + chatAfter: int("chatAfter"), + provider: varchar("provider", { length: 45 }), + providerKey: varchar("providerKey", { length: 255 }), + videoUrl: varchar("videoUrl", { length: 5000 }), + timezoneOffset: int("timezoneOffset"), + recurring: boolean("recurring"), + label: varchar("label", { length: 255 }), + sermonId: char("sermonId", { length: 11 }), +}); diff --git a/src/db/schema/doing.ts b/src/db/schema/doing.ts new file mode 100644 index 00000000..e9c6945a --- /dev/null +++ b/src/db/schema/doing.ts @@ -0,0 +1,183 @@ +import { mysqlTable, char, varchar, datetime, date, boolean, int, float, text, mediumtext, index } from "drizzle-orm/mysql-core"; + +export const actions = mysqlTable("actions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + automationId: char("automationId", { length: 11 }), + actionType: varchar("actionType", { length: 45 }), + actionData: mediumtext("actionData"), +}); + +export const assignments = mysqlTable("assignments", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + positionId: char("positionId", { length: 11 }), + personId: char("personId", { length: 11 }), + status: varchar("status", { length: 45 }), + notified: datetime("notified"), +}, (t) => [ + index("idx_church_person").on(t.churchId, t.personId), + index("idx_position").on(t.positionId), +]); + +export const automations = mysqlTable("automations", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + title: varchar("title", { length: 45 }), + recurs: varchar("recurs", { length: 45 }), + active: boolean("active"), +}); + +export const blockoutDates = mysqlTable("blockoutDates", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + startDate: date("startDate"), + endDate: date("endDate"), +}); + +export const conditions = mysqlTable("conditions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + conjunctionId: char("conjunctionId", { length: 11 }), + field: varchar("field", { length: 45 }), + fieldData: mediumtext("fieldData"), + operator: varchar("operator", { length: 45 }), + value: varchar("value", { length: 45 }), + label: varchar("label", { length: 255 }), +}); + +export const conjunctions = mysqlTable("conjunctions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + automationId: char("automationId", { length: 11 }), + parentId: char("parentId", { length: 11 }), + groupType: varchar("groupType", { length: 45 }), +}); + +export const contentProviderAuths = mysqlTable("contentProviderAuths", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + ministryId: char("ministryId", { length: 11 }), + providerId: varchar("providerId", { length: 50 }), + accessToken: text("accessToken"), + refreshToken: text("refreshToken"), + tokenType: varchar("tokenType", { length: 50 }), + expiresAt: datetime("expiresAt"), + scope: varchar("scope", { length: 255 }), +}, (t) => [ + index("idx_ministry_provider").on(t.churchId, t.ministryId, t.providerId), +]); + +export const notes = mysqlTable("notes", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + contentType: varchar("contentType", { length: 50 }), + contentId: char("contentId", { length: 11 }), + noteType: varchar("noteType", { length: 50 }), + addedBy: char("addedBy", { length: 11 }), + createdAt: datetime("createdAt"), + updatedAt: datetime("updatedAt"), + contents: text("contents"), +}, (t) => [ + index("churchId").on(t.churchId), +]); + +export const planItems = mysqlTable("planItems", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + planId: char("planId", { length: 11 }), + parentId: char("parentId", { length: 11 }), + sort: float("sort"), + itemType: varchar("itemType", { length: 45 }), + relatedId: char("relatedId", { length: 11 }), + label: varchar("label", { length: 100 }), + description: varchar("description", { length: 1000 }), + seconds: int("seconds"), + link: varchar("link", { length: 1000 }), + providerId: varchar("providerId", { length: 50 }), + providerPath: varchar("providerPath", { length: 500 }), + providerContentPath: varchar("providerContentPath", { length: 50 }), + thumbnailUrl: varchar("thumbnailUrl", { length: 1024 }), +}, (t) => [ + index("idx_church_plan").on(t.churchId, t.planId), + index("idx_parent").on(t.parentId), +]); + +export const plans = mysqlTable("plans", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + ministryId: char("ministryId", { length: 11 }), + planTypeId: char("planTypeId", { length: 11 }), + name: varchar("name", { length: 45 }), + serviceDate: date("serviceDate"), + notes: mediumtext("notes"), + serviceOrder: boolean("serviceOrder"), + contentType: varchar("contentType", { length: 50 }), + contentId: char("contentId", { length: 11 }), + providerId: varchar("providerId", { length: 50 }), + providerPlanId: varchar("providerPlanId", { length: 100 }), + providerPlanName: varchar("providerPlanName", { length: 255 }), + signupDeadlineHours: int("signupDeadlineHours"), + showVolunteerNames: boolean("showVolunteerNames"), +}); + +export const planTypes = mysqlTable("planTypes", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + ministryId: char("ministryId", { length: 11 }), + name: varchar("name", { length: 255 }), +}); + +export const positions = mysqlTable("positions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + planId: char("planId", { length: 11 }), + categoryName: varchar("categoryName", { length: 45 }), + name: varchar("name", { length: 45 }), + count: int("count"), + groupId: char("groupId", { length: 11 }), + allowSelfSignup: boolean("allowSelfSignup"), + description: text("description"), +}, (t) => [ + index("idx_church_plan").on(t.churchId, t.planId), + index("idx_group").on(t.groupId), +]); + +export const tasks = mysqlTable("tasks", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + taskNumber: int("taskNumber"), + taskType: varchar("taskType", { length: 45 }), + dateCreated: datetime("dateCreated"), + dateClosed: datetime("dateClosed"), + associatedWithType: varchar("associatedWithType", { length: 45 }), + associatedWithId: char("associatedWithId", { length: 11 }), + associatedWithLabel: varchar("associatedWithLabel", { length: 45 }), + createdByType: varchar("createdByType", { length: 45 }), + createdById: char("createdById", { length: 11 }), + createdByLabel: varchar("createdByLabel", { length: 45 }), + assignedToType: varchar("assignedToType", { length: 45 }), + assignedToId: char("assignedToId", { length: 11 }), + assignedToLabel: varchar("assignedToLabel", { length: 45 }), + title: varchar("title", { length: 255 }), + status: varchar("status", { length: 45 }), + automationId: char("automationId", { length: 11 }), + conversationId: char("conversationId", { length: 11 }), + data: text("data"), +}, (t) => [ + index("idx_church_status").on(t.churchId, t.status), + index("idx_automation").on(t.churchId, t.automationId), + index("idx_assigned").on(t.churchId, t.assignedToType, t.assignedToId), + index("idx_created").on(t.churchId, t.createdByType, t.createdById), +]); + +export const times = mysqlTable("times", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + planId: char("planId", { length: 11 }), + displayName: varchar("displayName", { length: 45 }), + startTime: datetime("startTime"), + endTime: datetime("endTime"), + teams: varchar("teams", { length: 1000 }), +}); diff --git a/src/db/schema/giving.ts b/src/db/schema/giving.ts new file mode 100644 index 00000000..fc11fd59 --- /dev/null +++ b/src/db/schema/giving.ts @@ -0,0 +1,141 @@ +import { mysqlTable, char, varchar, datetime, boolean, int, double, text, mediumtext, json, tinyint, index, uniqueIndex } from "drizzle-orm/mysql-core"; + +export const customers = mysqlTable("customers", { + id: varchar("id", { length: 255 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + provider: varchar("provider", { length: 50 }), + metadata: json("metadata"), +}); + +export const donationBatches = mysqlTable("donationBatches", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + name: varchar("name", { length: 50 }), + batchDate: datetime("batchDate"), +}, (t) => [ + index("idx_church_id").on(t.churchId), +]); + +export const donations = mysqlTable("donations", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + batchId: char("batchId", { length: 11 }), + personId: char("personId", { length: 11 }), + donationDate: datetime("donationDate"), + amount: double("amount"), + currency: varchar("currency", { length: 10 }), + method: varchar("method", { length: 50 }), + methodDetails: varchar("methodDetails", { length: 255 }), + notes: text("notes"), + entryTime: datetime("entryTime"), + status: varchar("status", { length: 20 }).default("complete"), + transactionId: varchar("transactionId", { length: 255 }), +}, (t) => [ + index("idx_church_donation_date").on(t.churchId, t.donationDate), + index("idx_church_person").on(t.churchId, t.personId), + index("idx_church_batch").on(t.churchId, t.batchId), + index("idx_church_method").on(t.churchId, t.method, t.methodDetails), + index("idx_church_status").on(t.churchId, t.status), + index("idx_transaction").on(t.transactionId), +]); + +export const eventLogs = mysqlTable("eventLogs", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + customerId: varchar("customerId", { length: 255 }), + provider: varchar("provider", { length: 50 }), + providerId: varchar("providerId", { length: 255 }), + status: varchar("status", { length: 50 }), + eventType: varchar("eventType", { length: 50 }), + message: text("message"), + created: datetime("created"), + resolved: tinyint("resolved"), +}, (t) => [ + index("idx_church_status_created").on(t.churchId, t.status, t.created), + index("idx_customer").on(t.customerId), + index("idx_provider_id").on(t.providerId), +]); + +export const fundDonations = mysqlTable("fundDonations", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + donationId: char("donationId", { length: 11 }), + fundId: char("fundId", { length: 11 }), + amount: double("amount"), +}, (t) => [ + index("idx_church_donation").on(t.churchId, t.donationId), + index("idx_church_fund").on(t.churchId, t.fundId), +]); + +export const funds = mysqlTable("funds", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + name: varchar("name", { length: 50 }), + removed: boolean("removed"), + productId: varchar("productId", { length: 50 }), + taxDeductible: boolean("taxDeductible"), +}, (t) => [ + index("idx_church_removed").on(t.churchId, t.removed), +]); + +export const gatewayPaymentMethods = mysqlTable("gatewayPaymentMethods", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }).notNull(), + gatewayId: char("gatewayId", { length: 11 }).notNull(), + customerId: varchar("customerId", { length: 255 }).notNull(), + externalId: varchar("externalId", { length: 255 }).notNull(), + methodType: varchar("methodType", { length: 50 }), + displayName: varchar("displayName", { length: 255 }), + metadata: json("metadata"), + createdAt: datetime("createdAt"), + updatedAt: datetime("updatedAt"), +}, (t) => [ + uniqueIndex("ux_gateway_payment_methods_external").on(t.gatewayId, t.externalId), + index("idx_gateway_payment_methods_church").on(t.churchId), + index("idx_gateway_payment_methods_customer").on(t.customerId), +]); + +export const gateways = mysqlTable("gateways", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + provider: varchar("provider", { length: 50 }), + publicKey: varchar("publicKey", { length: 255 }), + privateKey: varchar("privateKey", { length: 255 }), + webhookKey: varchar("webhookKey", { length: 255 }), + productId: varchar("productId", { length: 255 }), + payFees: boolean("payFees"), + currency: varchar("currency", { length: 10 }), + settings: json("settings"), + environment: varchar("environment", { length: 50 }), + createdAt: datetime("createdAt"), + updatedAt: datetime("updatedAt"), +}); + +export const givingSettings = mysqlTable("settings", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + keyName: varchar("keyName", { length: 255 }), + value: mediumtext("value"), + public: boolean("public"), +}, (t) => [ + index("churchId").on(t.churchId), +]); + +export const subscriptionFunds = mysqlTable("subscriptionFunds", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: varchar("churchId", { length: 11 }).notNull(), + subscriptionId: varchar("subscriptionId", { length: 255 }), + fundId: char("fundId", { length: 11 }), + amount: double("amount"), +}, (t) => [ + index("idx_church_subscription").on(t.churchId, t.subscriptionId), + index("idx_church_fund").on(t.churchId, t.fundId), +]); + +export const subscriptions = mysqlTable("subscriptions", { + id: varchar("id", { length: 255 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + customerId: varchar("customerId", { length: 255 }), +}); diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts new file mode 100644 index 00000000..4a6af0c0 --- /dev/null +++ b/src/db/schema/index.ts @@ -0,0 +1,6 @@ +export * as attendance from "./attendance.js"; +export * as content from "./content.js"; +export * as doing from "./doing.js"; +export * as giving from "./giving.js"; +export * as membership from "./membership.js"; +export * as messaging from "./messaging.js"; diff --git a/src/db/schema/membership.ts b/src/db/schema/membership.ts new file mode 100644 index 00000000..f89db9f5 --- /dev/null +++ b/src/db/schema/membership.ts @@ -0,0 +1,396 @@ +import { mysqlTable, char, varchar, datetime, date, boolean, int, float, text, mediumtext, tinyint, mysqlEnum, index, uniqueIndex } from "drizzle-orm/mysql-core"; + +export const accessLogs = mysqlTable("accessLogs", { + id: char("id", { length: 11 }).notNull().primaryKey(), + userId: char("userId", { length: 11 }), + churchId: char("churchId", { length: 11 }), + appName: varchar("appName", { length: 45 }), + loginTime: datetime("loginTime"), +}); + +export const answers = mysqlTable("answers", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + formSubmissionId: char("formSubmissionId", { length: 11 }), + questionId: char("questionId", { length: 11 }), + value: varchar("value", { length: 4000 }), +}, (t) => [ + index("churchId").on(t.churchId), + index("formSubmissionId").on(t.formSubmissionId), + index("questionId").on(t.questionId), +]); + +export const auditLogs = mysqlTable("auditLogs", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }).notNull(), + userId: char("userId", { length: 11 }), + category: varchar("category", { length: 50 }).notNull(), + action: varchar("action", { length: 100 }).notNull(), + entityType: varchar("entityType", { length: 100 }), + entityId: char("entityId", { length: 11 }), + details: text("details"), + ipAddress: varchar("ipAddress", { length: 45 }), + created: datetime("created").notNull(), +}, (t) => [ + index("ix_auditLogs_church_created").on(t.churchId, t.created), + index("ix_auditLogs_church_category").on(t.churchId, t.category), + index("ix_auditLogs_church_userId").on(t.churchId, t.userId), + index("ix_auditLogs_church_entity").on(t.churchId, t.entityType, t.entityId), +]); + +export const churches = mysqlTable("churches", { + id: char("id", { length: 11 }).notNull().primaryKey(), + name: varchar("name", { length: 255 }), + subDomain: varchar("subDomain", { length: 45 }), + registrationDate: datetime("registrationDate"), + address1: varchar("address1", { length: 255 }), + address2: varchar("address2", { length: 255 }), + city: varchar("city", { length: 255 }), + state: varchar("state", { length: 45 }), + zip: varchar("zip", { length: 45 }), + country: varchar("country", { length: 45 }), + archivedDate: datetime("archivedDate"), + latitude: float("latitude"), + longitude: float("longitude"), +}); + +export const clientErrors = mysqlTable("clientErrors", { + id: char("id", { length: 11 }).notNull().primaryKey(), + application: varchar("application", { length: 45 }), + errorTime: datetime("errorTime"), + userId: char("userId", { length: 11 }), + churchId: char("churchId", { length: 11 }), + originUrl: varchar("originUrl", { length: 255 }), + errorType: varchar("errorType", { length: 45 }), + message: varchar("message", { length: 255 }), + details: text("details"), +}); + +export const domains = mysqlTable("domains", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + domainName: varchar("domainName", { length: 255 }), + lastChecked: datetime("lastChecked"), + isStale: tinyint("isStale").default(0), +}); + +export const forms = mysqlTable("forms", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + name: varchar("name", { length: 255 }), + contentType: varchar("contentType", { length: 50 }), + createdTime: datetime("createdTime"), + modifiedTime: datetime("modifiedTime"), + accessStartTime: datetime("accessStartTime"), + accessEndTime: datetime("accessEndTime"), + restricted: boolean("restricted"), + archived: boolean("archived"), + removed: boolean("removed"), + thankYouMessage: text("thankYouMessage"), +}, (t) => [ + index("churchId").on(t.churchId), + index("churchId_removed_archived").on(t.churchId, t.removed, t.archived), + index("churchId_id").on(t.churchId, t.id), +]); + +export const formSubmissions = mysqlTable("formSubmissions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + formId: char("formId", { length: 11 }), + contentType: varchar("contentType", { length: 50 }), + contentId: char("contentId", { length: 11 }), + submissionDate: datetime("submissionDate"), + submittedBy: char("submittedBy", { length: 11 }), + revisionDate: datetime("revisionDate"), + revisedBy: char("revisedBy", { length: 11 }), +}, (t) => [ + index("churchId").on(t.churchId), + index("formId").on(t.formId), +]); + +export const groupMembers = mysqlTable("groupMembers", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + groupId: char("groupId", { length: 11 }), + personId: char("personId", { length: 11 }), + joinDate: datetime("joinDate"), + leader: boolean("leader"), +}, (t) => [ + index("churchId").on(t.churchId), + index("groupId").on(t.groupId), + index("personId").on(t.personId), + index("churchId_groupId_personId").on(t.churchId, t.groupId, t.personId), + index("personId_churchId").on(t.personId, t.churchId), +]); + +export const groups = mysqlTable("groups", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + categoryName: varchar("categoryName", { length: 50 }), + name: varchar("name", { length: 50 }), + trackAttendance: boolean("trackAttendance"), + parentPickup: boolean("parentPickup"), + printNametag: boolean("printNametag"), + about: text("about"), + photoUrl: varchar("photoUrl", { length: 255 }), + removed: boolean("removed"), + tags: varchar("tags", { length: 45 }), + meetingTime: varchar("meetingTime", { length: 45 }), + meetingLocation: varchar("meetingLocation", { length: 45 }), + labels: varchar("labels", { length: 500 }), + slug: varchar("slug", { length: 45 }), +}, (t) => [ + index("churchId").on(t.churchId), + index("churchId_removed_tags").on(t.churchId, t.removed, t.tags), + index("churchId_removed_labels").on(t.churchId, t.removed, t.labels), +]); + +export const households = mysqlTable("households", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + name: varchar("name", { length: 50 }), +}, (t) => [ + index("churchId").on(t.churchId), +]); + +export const memberPermissions = mysqlTable("memberPermissions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + memberId: char("memberId", { length: 11 }), + contentType: varchar("contentType", { length: 45 }), + contentId: char("contentId", { length: 11 }), + action: varchar("action", { length: 45 }), + emailNotification: boolean("emailNotification"), +}, (t) => [ + index("churchId_contentId_memberId").on(t.churchId, t.contentId, t.memberId), +]); + +export const membershipNotes = mysqlTable("notes", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + contentType: varchar("contentType", { length: 50 }), + contentId: char("contentId", { length: 11 }), + noteType: varchar("noteType", { length: 50 }), + addedBy: char("addedBy", { length: 11 }), + createdAt: datetime("createdAt"), + contents: text("contents"), + updatedAt: datetime("updatedAt"), +}, (t) => [ + index("churchId").on(t.churchId), +]); + +export const oAuthClients = mysqlTable("oAuthClients", { + id: char("id", { length: 11 }).notNull().primaryKey(), + name: varchar("name", { length: 45 }), + clientId: varchar("clientId", { length: 45 }), + clientSecret: varchar("clientSecret", { length: 45 }), + redirectUris: varchar("redirectUris", { length: 255 }), + scopes: varchar("scopes", { length: 255 }), + createdAt: datetime("createdAt"), +}); + +export const oAuthCodes = mysqlTable("oAuthCodes", { + id: char("id", { length: 11 }).notNull().primaryKey(), + userChurchId: char("userChurchId", { length: 11 }), + clientId: char("clientId", { length: 11 }), + code: varchar("code", { length: 45 }), + redirectUri: varchar("redirectUri", { length: 255 }), + scopes: varchar("scopes", { length: 255 }), + expiresAt: datetime("expiresAt"), + createdAt: datetime("createdAt"), +}); + +export const oAuthDeviceCodes = mysqlTable("oAuthDeviceCodes", { + id: char("id", { length: 11 }).notNull().primaryKey(), + deviceCode: varchar("deviceCode", { length: 64 }).notNull(), + userCode: varchar("userCode", { length: 16 }).notNull(), + clientId: varchar("clientId", { length: 45 }).notNull(), + scopes: varchar("scopes", { length: 255 }), + expiresAt: datetime("expiresAt").notNull(), + pollInterval: int("pollInterval").default(5), + status: mysqlEnum("status", ["pending", "approved", "denied", "expired"]).default("pending"), + approvedByUserId: char("approvedByUserId", { length: 11 }), + userChurchId: char("userChurchId", { length: 11 }), + churchId: char("churchId", { length: 11 }), + createdAt: datetime("createdAt"), +}, (t) => [ + uniqueIndex("deviceCode").on(t.deviceCode), + index("userCode_status").on(t.userCode, t.status), + index("status_expiresAt").on(t.status, t.expiresAt), +]); + +export const oAuthRelaySessions = mysqlTable("oAuthRelaySessions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + sessionCode: varchar("sessionCode", { length: 16 }).notNull(), + provider: varchar("provider", { length: 45 }).notNull(), + authCode: varchar("authCode", { length: 512 }), + redirectUri: varchar("redirectUri", { length: 512 }).notNull(), + status: mysqlEnum("status", ["pending", "completed", "expired"]).default("pending"), + expiresAt: datetime("expiresAt").notNull(), + createdAt: datetime("createdAt"), +}, (t) => [ + uniqueIndex("sessionCode").on(t.sessionCode), + index("status_expiresAt").on(t.status, t.expiresAt), +]); + +export const oAuthTokens = mysqlTable("oAuthTokens", { + id: char("id", { length: 11 }).notNull().primaryKey(), + clientId: char("clientId", { length: 11 }), + userChurchId: char("userChurchId", { length: 11 }), + accessToken: varchar("accessToken", { length: 1000 }), + refreshToken: varchar("refreshToken", { length: 45 }), + scopes: varchar("scopes", { length: 45 }), + expiresAt: datetime("expiresAt"), + createdAt: datetime("createdAt"), +}); + +export const people = mysqlTable("people", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + userId: char("userId", { length: 11 }), + displayName: varchar("displayName", { length: 100 }), + firstName: varchar("firstName", { length: 50 }), + middleName: varchar("middleName", { length: 50 }), + lastName: varchar("lastName", { length: 50 }), + nickName: varchar("nickName", { length: 50 }), + prefix: varchar("prefix", { length: 10 }), + suffix: varchar("suffix", { length: 10 }), + birthDate: datetime("birthDate"), + gender: varchar("gender", { length: 11 }), + maritalStatus: varchar("maritalStatus", { length: 10 }), + anniversary: datetime("anniversary"), + membershipStatus: varchar("membershipStatus", { length: 50 }), + homePhone: varchar("homePhone", { length: 21 }), + mobilePhone: varchar("mobilePhone", { length: 21 }), + workPhone: varchar("workPhone", { length: 21 }), + email: varchar("email", { length: 100 }), + address1: varchar("address1", { length: 50 }), + address2: varchar("address2", { length: 50 }), + city: varchar("city", { length: 30 }), + state: varchar("state", { length: 10 }), + zip: varchar("zip", { length: 10 }), + photoUpdated: datetime("photoUpdated"), + householdId: char("householdId", { length: 11 }), + householdRole: varchar("householdRole", { length: 10 }), + removed: boolean("removed"), + conversationId: char("conversationId", { length: 11 }), + optedOut: boolean("optedOut"), + nametagNotes: varchar("nametagNotes", { length: 20 }), + donorNumber: varchar("donorNumber", { length: 20 }), +}, (t) => [ + index("churchId").on(t.churchId), + index("userId").on(t.userId), + index("householdId").on(t.householdId), +]); + +export const questions = mysqlTable("questions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + formId: char("formId", { length: 11 }), + parentId: char("parentId", { length: 11 }), + title: varchar("title", { length: 255 }), + description: varchar("description", { length: 255 }), + fieldType: varchar("fieldType", { length: 50 }), + placeholder: varchar("placeholder", { length: 50 }), + sort: int("sort"), + choices: text("choices"), + removed: boolean("removed"), + required: boolean("required"), +}, (t) => [ + index("churchId").on(t.churchId), + index("formId").on(t.formId), +]); + +export const roleMembers = mysqlTable("roleMembers", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + roleId: char("roleId", { length: 11 }), + userId: char("userId", { length: 11 }), + dateAdded: datetime("dateAdded"), + addedBy: char("addedBy", { length: 11 }), +}, (t) => [ + index("userId_INDEX").on(t.userId), + index("userId_churchId").on(t.userId, t.churchId), + index("roleId_churchId").on(t.roleId, t.churchId), +]); + +export const rolePermissions = mysqlTable("rolePermissions", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + roleId: char("roleId", { length: 11 }), + apiName: varchar("apiName", { length: 45 }), + contentType: varchar("contentType", { length: 45 }), + contentId: char("contentId", { length: 11 }), + action: varchar("action", { length: 45 }), +}, (t) => [ + index("roleId_churchId_INDEX").on(t.roleId, t.churchId), +]); + +export const roles = mysqlTable("roles", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + name: varchar("name", { length: 255 }), +}); + +export const membershipSettings = mysqlTable("settings", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + userId: char("userId", { length: 11 }), + keyName: varchar("keyName", { length: 255 }), + value: mediumtext("value"), + public: boolean("public"), +}, (t) => [ + index("churchId").on(t.churchId), +]); + +export const usageTrends = mysqlTable("usageTrends", { + id: char("id", { length: 11 }).notNull().primaryKey(), + year: int("year"), + week: int("week"), + b1Users: int("b1Users"), + b1Churches: int("b1Churches"), + b1Devices: int("b1Devices"), + chumsUsers: int("chumsUsers"), + chumsChurches: int("chumsChurches"), + lessonsUsers: int("lessonsUsers"), + lessonsChurches: int("lessonsChurches"), + lessonsDevices: int("lessonsDevices"), + freeShowDevices: int("freeShowDevices"), +}, (t) => [ + uniqueIndex("year_week").on(t.year, t.week), +]); + +export const userChurches = mysqlTable("userChurches", { + id: char("id", { length: 11 }).notNull().primaryKey(), + userId: char("userId", { length: 11 }), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + lastAccessed: datetime("lastAccessed"), +}, (t) => [ + index("userId").on(t.userId), + index("churchId").on(t.churchId), +]); + +export const users = mysqlTable("users", { + id: char("id", { length: 11 }).notNull().primaryKey(), + email: varchar("email", { length: 191 }), + password: varchar("password", { length: 255 }), + authGuid: varchar("authGuid", { length: 255 }), + displayName: varchar("displayName", { length: 255 }), + registrationDate: datetime("registrationDate"), + lastLogin: datetime("lastLogin"), + firstName: varchar("firstName", { length: 45 }), + lastName: varchar("lastName", { length: 45 }), +}, (t) => [ + uniqueIndex("email_UNIQUE").on(t.email), + index("authGuid_INDEX").on(t.authGuid), +]); + +export const visibilityPreferences = mysqlTable("visibilityPreferences", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + address: varchar("address", { length: 50 }), + phoneNumber: varchar("phoneNumber", { length: 50 }), + email: varchar("email", { length: 50 }), +}); diff --git a/src/db/schema/messaging.ts b/src/db/schema/messaging.ts new file mode 100644 index 00000000..17ca6e95 --- /dev/null +++ b/src/db/schema/messaging.ts @@ -0,0 +1,183 @@ +import { mysqlTable, char, varchar, datetime, boolean, int, text, index } from "drizzle-orm/mysql-core"; + +export const blockedIps = mysqlTable("blockedIps", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + conversationId: char("conversationId", { length: 11 }), + serviceId: char("serviceId", { length: 11 }), + ipAddress: varchar("ipAddress", { length: 45 }), +}); + +export const connections = mysqlTable("connections", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + conversationId: char("conversationId", { length: 11 }), + personId: char("personId", { length: 11 }), + displayName: varchar("displayName", { length: 45 }), + timeJoined: datetime("timeJoined"), + socketId: varchar("socketId", { length: 45 }), + ipAddress: varchar("ipAddress", { length: 45 }), +}, (t) => [ + index("ix_churchId").on(t.churchId, t.conversationId), +]); + +export const conversations = mysqlTable("conversations", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + contentType: varchar("contentType", { length: 45 }), + contentId: varchar("contentId", { length: 255 }), + title: varchar("title", { length: 255 }), + dateCreated: datetime("dateCreated"), + groupId: char("groupId", { length: 11 }), + visibility: varchar("visibility", { length: 45 }), + firstPostId: char("firstPostId", { length: 11 }), + lastPostId: char("lastPostId", { length: 11 }), + postCount: int("postCount"), + allowAnonymousPosts: boolean("allowAnonymousPosts"), +}, (t) => [ + index("ix_churchId").on(t.churchId, t.contentType, t.contentId), +]); + +export const deliveryLogs = mysqlTable("deliveryLogs", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + contentType: varchar("contentType", { length: 20 }), + contentId: char("contentId", { length: 11 }), + deliveryMethod: varchar("deliveryMethod", { length: 10 }), + success: boolean("success"), + errorMessage: varchar("errorMessage", { length: 500 }), + deliveryAddress: varchar("deliveryAddress", { length: 255 }), + attemptTime: datetime("attemptTime"), +}, (t) => [ + index("ix_content").on(t.contentType, t.contentId), + index("ix_personId").on(t.personId, t.attemptTime), + index("ix_churchId_time").on(t.churchId, t.attemptTime), +]); + +export const deviceContents = mysqlTable("deviceContents", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + deviceId: char("deviceId", { length: 11 }), + contentType: varchar("contentType", { length: 45 }), + contentId: char("contentId", { length: 11 }), +}); + +export const devices = mysqlTable("devices", { + id: char("id", { length: 11 }).notNull().primaryKey(), + appName: varchar("appName", { length: 20 }), + deviceId: varchar("deviceId", { length: 45 }), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + fcmToken: varchar("fcmToken", { length: 255 }), + label: varchar("label", { length: 45 }), + registrationDate: datetime("registrationDate"), + lastActiveDate: datetime("lastActiveDate"), + deviceInfo: text("deviceInfo"), + admId: varchar("admId", { length: 255 }), + pairingCode: varchar("pairingCode", { length: 45 }), + ipAddress: varchar("ipAddress", { length: 45 }), + contentType: varchar("contentType", { length: 45 }), + contentId: char("contentId", { length: 11 }), +}, (t) => [ + index("appName_deviceId").on(t.appName, t.deviceId), + index("personId_lastActiveDate").on(t.personId, t.lastActiveDate), + index("fcmToken").on(t.fcmToken), + index("pairingCode").on(t.pairingCode), +]); + +export const emailTemplates = mysqlTable("emailTemplates", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }).notNull(), + name: varchar("name", { length: 255 }).notNull(), + subject: varchar("subject", { length: 500 }).notNull(), + htmlContent: text("htmlContent").notNull(), + category: varchar("category", { length: 100 }), + dateCreated: datetime("dateCreated"), + dateModified: datetime("dateModified"), +}, (t) => [ + index("ix_churchId").on(t.churchId), +]); + +export const messages = mysqlTable("messages", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + conversationId: char("conversationId", { length: 11 }), + displayName: varchar("displayName", { length: 45 }), + timeSent: datetime("timeSent"), + messageType: varchar("messageType", { length: 45 }), + content: text("content"), + personId: char("personId", { length: 11 }), + timeUpdated: datetime("timeUpdated"), +}, (t) => [ + index("ix_churchId").on(t.churchId, t.conversationId), + index("ix_timeSent").on(t.timeSent), + index("ix_personId").on(t.personId), +]); + +export const notificationPreferences = mysqlTable("notificationPreferences", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + allowPush: boolean("allowPush"), + emailFrequency: varchar("emailFrequency", { length: 10 }), +}); + +export const notifications = mysqlTable("notifications", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + personId: char("personId", { length: 11 }), + contentType: varchar("contentType", { length: 45 }), + contentId: char("contentId", { length: 11 }), + timeSent: datetime("timeSent"), + isNew: boolean("isNew"), + message: text("message"), + link: varchar("link", { length: 100 }), + deliveryMethod: varchar("deliveryMethod", { length: 10 }), + triggeredByPersonId: char("triggeredByPersonId", { length: 11 }), +}, (t) => [ + index("churchId_personId_timeSent").on(t.churchId, t.personId, t.timeSent), + index("isNew").on(t.isNew), +]); + +export const privateMessages = mysqlTable("privateMessages", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }), + fromPersonId: char("fromPersonId", { length: 11 }), + toPersonId: char("toPersonId", { length: 11 }), + conversationId: char("conversationId", { length: 11 }), + notifyPersonId: char("notifyPersonId", { length: 11 }), + deliveryMethod: varchar("deliveryMethod", { length: 10 }), +}, (t) => [ + index("IX_churchFrom").on(t.churchId, t.fromPersonId), + index("IX_churchTo").on(t.churchId, t.toPersonId), + index("IX_notifyPersonId").on(t.churchId, t.notifyPersonId), + index("IX_conversationId").on(t.conversationId), +]); + +export const sentTexts = mysqlTable("sentTexts", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }).notNull(), + groupId: char("groupId", { length: 11 }), + recipientPersonId: char("recipientPersonId", { length: 11 }), + senderPersonId: char("senderPersonId", { length: 11 }), + message: varchar("message", { length: 1600 }), + recipientCount: int("recipientCount").default(0), + successCount: int("successCount").default(0), + failCount: int("failCount").default(0), + timeSent: datetime("timeSent"), +}, (t) => [ + index("ix_churchId").on(t.churchId, t.timeSent), +]); + +export const textingProviders = mysqlTable("textingProviders", { + id: char("id", { length: 11 }).notNull().primaryKey(), + churchId: char("churchId", { length: 11 }).notNull(), + provider: varchar("provider", { length: 50 }).notNull(), + apiKey: varchar("apiKey", { length: 500 }), + apiSecret: varchar("apiSecret", { length: 500 }), + fromNumber: varchar("fromNumber", { length: 20 }), + enabled: boolean("enabled"), +}, (t) => [ + index("ix_churchId").on(t.churchId), +]); From 066ae4f9256e6005210cf9261f3378af654c9d0a Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Tue, 10 Mar 2026 20:56:57 -0600 Subject: [PATCH 02/19] feat(drizzle): add DrizzleRepo base classes, remove dead infrastructure Add 3-tier base class hierarchy: - BaseDrizzleRepo: db connection + executeRows() helper - DrizzleRepo: standard CRUD for tables with id + churchId columns, with opt-in soft-delete support (protected readonly softDelete = true) - GlobalDrizzleRepo: CRUD for global tables with id only (no churchId) Remove ConfiguredRepo and GlobalConfiguredRepo (replaced by DrizzleRepo). Update barrel exports in shared/infrastructure/index.ts. Co-Authored-By: Claude Opus 4.6 --- src/shared/infrastructure/ConfiguredRepo.ts | 128 ---------------- src/shared/infrastructure/DrizzleRepo.ts | 114 ++++++++++++++ .../infrastructure/GlobalConfiguredRepo.ts | 140 ------------------ src/shared/infrastructure/index.ts | 2 +- 4 files changed, 115 insertions(+), 269 deletions(-) delete mode 100644 src/shared/infrastructure/ConfiguredRepo.ts create mode 100644 src/shared/infrastructure/DrizzleRepo.ts delete mode 100644 src/shared/infrastructure/GlobalConfiguredRepo.ts diff --git a/src/shared/infrastructure/ConfiguredRepo.ts b/src/shared/infrastructure/ConfiguredRepo.ts deleted file mode 100644 index 15ffaf0e..00000000 --- a/src/shared/infrastructure/ConfiguredRepo.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { TypedDB } from "./TypedDB.js"; -import { BaseRepo } from "./BaseRepo.js"; - -export interface RepoConfig { - tableName: string; - defaultOrderBy?: string; - hasSoftDelete?: boolean; - idColumn?: string; - churchIdColumn?: string; - removedColumn?: string; - // New pattern: Single column list used for both insert and update operations - // This is the primary column configuration that should be used going forward - columns?: (keyof T | string)[]; - // Legacy properties: Optional overrides for backward compatibility - // If not provided, insertColumns and updateColumns will fall back to using 'columns' - insertColumns?: (keyof T | string)[]; - updateColumns?: (keyof T | string)[]; - insertLiterals?: Record; // column -> SQL literal (e.g., "NOW()", "0") - updateLiterals?: Record; -} - -export abstract class ConfiguredRepo extends BaseRepo { - protected abstract get repoConfig(): RepoConfig; - - protected getHasSoftDelete(): boolean { - return this.repoConfig.hasSoftDelete !== false; - } - - protected getDefaultOrderBy(): string | undefined { - return this.repoConfig.defaultOrderBy; - } - - protected async create(model: T): Promise { - const m: any = model as any; - if (!m[this.idColumn]) m[this.idColumn] = this.createId(); - const { sql, params } = this.buildInsert(model); - //console.log(sql, params); - await TypedDB.query(sql, params); - return model; - } - - protected async update(model: T): Promise { - const { sql, params } = this.buildUpdate(model); - await TypedDB.query(sql, params); - return model; - } - - protected buildInsert(model: T): { sql: string; params: any[] } { - const cfg = this.repoConfig; - // Use insertColumns override if provided, otherwise use columns, fallback to insertColumns for backward compatibility - const insertCols = cfg.insertColumns || cfg.columns || []; - const cols: string[] = [this.idColumn, ...(this.churchIdColumn ? [this.churchIdColumn] : []), ...insertCols.map(String)]; - const literals = cfg.insertLiterals || {}; - Object.keys(literals).forEach((c) => { - if (!cols.includes(c)) cols.push(c); - }); - - const placeholders: string[] = []; - const params: any[] = []; - cols.forEach((col) => { - if (literals[col] !== undefined) { - placeholders.push(literals[col]); - } else { - placeholders.push("?"); - params.push((model as any)[col]); - } - }); - - const sql = `INSERT INTO \`${this.table()}\` (${cols.join(", ")}) VALUES (${placeholders.join(", ")});`; - return { sql, params }; - } - - protected buildUpdate(model: T): { sql: string; params: any[] } { - const cfg = this.repoConfig; - const sets: string[] = []; - const params: any[] = []; - const literals = cfg.updateLiterals || {}; - - // Use updateColumns override if provided, otherwise use columns, fallback to updateColumns for backward compatibility - const updateCols = cfg.updateColumns || cfg.columns || []; - updateCols.forEach((c) => { - const col = String(c); - sets.push(`${col}=?`); - params.push((model as any)[col]); - }); - Object.keys(literals).forEach((c) => { - sets.push(`${c}=${literals[c]}`); - }); - - const where = this.churchIdColumn - ? ` WHERE ${this.idColumn}=? and ${this.churchIdColumn}=?` - : ` WHERE ${this.idColumn}=?`; - params.push((model as any)[this.idColumn]); - if (this.churchIdColumn) params.push((model as any)[this.churchIdColumn]); - const sql = `UPDATE \`${this.table()}\` SET ${sets.join(", ")} ${where}`; - return { sql, params }; - } - - // Make BaseRepository use config's table name - protected table(): string { - return this.repoConfig.tableName; - } - - // Respect config soft-delete & default order when using inherited helpers - public async delete(churchId: string, id: string) { - const cfg = this.repoConfig; - if (cfg.hasSoftDelete === false) { - const sql = `DELETE FROM \`${this.table()}\` WHERE ${this.idColumn}=? AND ${this.churchIdColumn}=?;`; - return TypedDB.query(sql, [id, churchId]); - } - return this.deleteSoft(churchId, id); - } - - public saveAll(models: T[]) { - const promises: Promise[] = []; - models.forEach((model) => { - promises.push(this.save(model)); - }); - return Promise.all(promises); - } - - /** - * Force insert a model, even if it has an id (bypasses save() update logic) - */ - public insert(model: T): Promise { - return this.create(model); - } -} diff --git a/src/shared/infrastructure/DrizzleRepo.ts b/src/shared/infrastructure/DrizzleRepo.ts new file mode 100644 index 00000000..32f0d7c6 --- /dev/null +++ b/src/shared/infrastructure/DrizzleRepo.ts @@ -0,0 +1,114 @@ +import { eq, and } from "drizzle-orm"; +import type { MySqlTableWithColumns } from "drizzle-orm/mysql-core"; +import type { MySql2Database } from "drizzle-orm/mysql2"; +import { UniqueIdHelper } from "@churchapps/apihelper"; +import { getDrizzleDb } from "../../db/drizzle.js"; + +/** + * Base for any repo backed by a Drizzle table in a named module. + * Provides the db connection and `executeRows()` helper. + * + * Use `DrizzleRepo` (below) for tables with `id` + `churchId`. + * Use `GlobalDrizzleRepo` for tables with `id` only (no churchId). + */ +abstract class BaseDrizzleRepo> { + protected abstract readonly table: TTable; + protected abstract readonly moduleName: string; + + protected get db(): MySql2Database { + return getDrizzleDb(this.moduleName); + } + + /** + * Execute a raw SQL query and return the rows array. + * Unwraps the mysql2 [rows, fields] tuple that db.execute() returns. + */ + protected async executeRows(query: any): Promise { + const result = await this.db.execute(query); + return (Array.isArray(result) && Array.isArray(result[0])) ? result[0] : result as any[]; + } + + /** Default passthrough — override in repos that need row-to-model transformation. */ + public convertToModel(_churchId: string, data: any) { return data; } + public convertAllToModel(_churchId: string, data: any) { return Array.isArray(data) ? data : []; } +} + +/** + * Base repository for tables that have `id` and `churchId` columns. + * Provides standard CRUD: save, delete, load, loadOne, loadAll. + * + * Set `protected readonly softDelete = true` in subclasses whose table has a + * `removed` column. When enabled: + * - `delete()` sets `removed = 1` instead of deleting the row. + * - `load()`, `loadOne()`, and `loadAll()` automatically exclude removed rows. + */ +export abstract class DrizzleRepo> extends BaseDrizzleRepo { + /** Override to `true` for tables with a `removed` column. */ + protected readonly softDelete: boolean = false; + + public async save(model: any) { + if (model.id) { + const { id: _id, churchId: _cid, ...setData } = model; + await this.db.update(this.table).set(setData).where(and(eq(this.table.id, model.id), eq(this.table.churchId, model.churchId))); + } else { + model.id = UniqueIdHelper.shortId(); + const values = this.softDelete ? { ...model, removed: false } : model; + await this.db.insert(this.table).values(values); + } + return model; + } + + public async delete(churchId: string, id: string) { + if (this.softDelete) { + await this.db.update(this.table).set({ removed: true } as any).where(and(eq(this.table.id, id), eq(this.table.churchId, churchId))); + } else { + await this.db.delete(this.table).where(and(eq(this.table.id, id), eq(this.table.churchId, churchId))); + } + } + + public loadOne(churchId: string, id: string): Promise { + const conditions = [eq(this.table.id, id), eq(this.table.churchId, churchId)]; + if (this.softDelete) conditions.push(eq(this.table.removed, false)); + return this.db.select().from(this.table).where(and(...conditions)).then(r => r[0] ?? null); + } + + public load(churchId: string, id: string): Promise { + return this.loadOne(churchId, id); + } + + public loadAll(churchId: string): Promise { + const conditions = [eq(this.table.churchId, churchId)]; + if (this.softDelete) conditions.push(eq(this.table.removed, false)); + return this.db.select().from(this.table).where(and(...conditions)); + } +} + +/** + * Base repository for global tables that have `id` but no `churchId`. + * Provides save, delete, load, loadAll keyed by `id` only. + */ +export abstract class GlobalDrizzleRepo> extends BaseDrizzleRepo { + + public async save(model: any) { + if (model.id) { + const { id: _id, ...setData } = model; + await this.db.update(this.table).set(setData).where(eq(this.table.id, model.id)); + } else { + model.id = UniqueIdHelper.shortId(); + await this.db.insert(this.table).values(model); + } + return model; + } + + public async delete(id: string) { + await this.db.delete(this.table).where(eq(this.table.id, id)); + } + + public load(id: string): Promise { + return this.db.select().from(this.table).where(eq(this.table.id, id)).then(r => r[0] ?? null); + } + + public loadAll(): Promise { + return this.db.select().from(this.table); + } +} diff --git a/src/shared/infrastructure/GlobalConfiguredRepo.ts b/src/shared/infrastructure/GlobalConfiguredRepo.ts deleted file mode 100644 index ae428d32..00000000 --- a/src/shared/infrastructure/GlobalConfiguredRepo.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { BaseRepo } from "./BaseRepo.js"; -import { TypedDB } from "./TypedDB.js"; - -export interface GlobalRepoConfig { - tableName: string; - defaultOrderBy?: string; - hasSoftDelete?: boolean; - hasChurchId?: boolean; // Whether the table has a churchId column - idColumn?: string; - // New pattern: Single column list used for both insert and update operations - // This is the primary column configuration that should be used going forward - columns?: (keyof T | string)[]; - // Legacy properties: Optional overrides for backward compatibility - // If not provided, insertColumns and updateColumns will fall back to using 'columns' - insertColumns?: (keyof T | string)[]; // Override for insert if different from columns - updateColumns?: (keyof T | string)[]; // Override for update if different from columns - insertLiterals?: Record; // column -> SQL literal (e.g., "NOW()", "0") - updateLiterals?: Record; -} - -export abstract class GlobalConfiguredRepo extends BaseRepo { - protected abstract get repoConfig(): GlobalRepoConfig; - - protected async create(model: T): Promise { - const m: any = model as any; - if (!m[this.idColumn]) m[this.idColumn] = this.createId(); - const { sql, params } = this.buildInsert(model); - await TypedDB.query(sql, params); - return model; - } - - protected async update(model: T): Promise { - const { sql, params } = this.buildUpdate(model); - await TypedDB.query(sql, params); - return model; - } - - public saveAll(models: T[]) { - const promises: Promise[] = []; - models.forEach((m) => { - promises.push(this.save(m)); - }); - return Promise.all(promises); - } - - protected buildInsert(model: T): { sql: string; params: any[] } { - const cfg = this.repoConfig; - // Use insertColumns override if provided, otherwise use columns, fallback to insertColumns for backward compatibility - const insertCols = cfg.insertColumns || cfg.columns || []; - const cols: string[] = [this.idColumn, ...insertCols.map(String)]; - - // Automatically include churchId if the table has it - if (cfg.hasChurchId && !cols.includes("churchId")) { - cols.push("churchId"); - } - - const literals = cfg.insertLiterals || {}; - Object.keys(literals).forEach((c) => { - if (!cols.includes(c)) cols.push(c); - }); - - const placeholders: string[] = []; - const params: any[] = []; - cols.forEach((col) => { - if (literals[col] !== undefined) { - placeholders.push(literals[col]); - } else { - placeholders.push("?"); - params.push((model as any)[col]); - } - }); - - const sql = `INSERT INTO ${this.table()} (${cols.join(", ")}) VALUES (${placeholders.join(", ")});`; - return { sql, params }; - } - - protected buildUpdate(model: T): { sql: string; params: any[] } { - const cfg = this.repoConfig; - const sets: string[] = []; - const params: any[] = []; - const literals = cfg.updateLiterals || {}; - - // Use updateColumns override if provided, otherwise use columns, fallback to updateColumns for backward compatibility - const updateCols = cfg.updateColumns || cfg.columns || []; - updateCols.forEach((c) => { - const col = String(c); - // Skip churchId in updates - it should never be changed after creation - if (cfg.hasChurchId && col === "churchId") { - return; - } - sets.push(`${col}=?`); - params.push((model as any)[col]); - }); - Object.keys(literals).forEach((c) => { - sets.push(`${c}=${literals[c]}`); - }); - - // Build WHERE clause - include churchId if the table has it - let where = ` WHERE ${this.idColumn}=?`; - params.push((model as any)[this.idColumn]); - - if (cfg.hasChurchId) { - where += " AND churchId=?"; - params.push((model as any).churchId); - } - - const sql = `UPDATE ${this.table()} SET ${sets.join(", ")} ${where}`; - return { sql, params }; - } - - // Make BaseRepository use config's table name - protected table(): string { - return this.repoConfig.tableName; - } - - // Global delete method (no churchId) - public async delete(id: string) { - const cfg = this.repoConfig; - if (cfg.hasSoftDelete === false) { - const sql = `DELETE FROM ${this.table()} WHERE ${this.idColumn}=?;`; - return TypedDB.query(sql, [id]); - } - throw new Error("Soft delete not implemented for global repositories"); - } - - // Global load method (no churchId) - public load(id: string) { - return TypedDB.queryOne(`SELECT * FROM ${this.table()} WHERE ${this.idColumn}=?;`, [id]); - } - - // Global loadMany method (no churchId) - public async loadMany(orderBy?: string) { - const cfg = this.repoConfig; - const order = orderBy || cfg.defaultOrderBy; - const orderClause = order ? ` ORDER BY ${order}` : ""; - const sql = `SELECT * FROM ${this.table()}${orderClause};`; - const result = await TypedDB.query(sql, []); - return result as any[]; - } -} diff --git a/src/shared/infrastructure/index.ts b/src/shared/infrastructure/index.ts index 0049d7a0..43a98ab4 100644 --- a/src/shared/infrastructure/index.ts +++ b/src/shared/infrastructure/index.ts @@ -9,5 +9,5 @@ export { CustomAuthProvider } from "./CustomAuthProvider.js"; export { BaseController } from "./BaseController.js"; export { DB, MultiDatabasePool } from "./DB.js"; export { BaseRepo } from "./BaseRepo.js"; -export { ConfiguredRepo, type RepoConfig } from "./ConfiguredRepo.js"; export { TypedDB } from "./TypedDB.js"; +export { DrizzleRepo, GlobalDrizzleRepo } from "./DrizzleRepo.js"; From f9cbf372b4c2013102d158639c04c7d543615f05 Mon Sep 17 00:00:00 2001 From: Don Kendall Date: Tue, 10 Mar 2026 20:57:06 -0600 Subject: [PATCH 03/19] feat(drizzle): migrate doing module repos to Drizzle ORM Convert all 13 doing module repositories from raw SQL (TypedDB) to Drizzle query builder: ActionRepo, AssignmentRepo, AutomationRepo, BlockoutDateRepo, ConditionRepo, ConjunctionRepo, ContentProviderAuthRepo, PlanItemRepo, PlanRepo, PlanTypeRepo, PositionRepo, TaskRepo, TimeRepo. Co-Authored-By: Claude Opus 4.6 --- src/modules/doing/repositories/ActionRepo.ts | 20 +- .../doing/repositories/AssignmentRepo.ts | 131 +++++---- .../doing/repositories/AutomationRepo.ts | 23 +- .../doing/repositories/BlockoutDateRepo.ts | 52 ++-- .../doing/repositories/ConditionRepo.ts | 23 +- .../doing/repositories/ConjunctionRepo.ts | 19 +- .../repositories/ContentProviderAuthRepo.ts | 39 +-- .../doing/repositories/PlanItemRepo.ts | 25 +- src/modules/doing/repositories/PlanRepo.ts | 125 +++----- .../doing/repositories/PlanTypeRepo.ts | 31 +- .../doing/repositories/PositionRepo.ts | 44 +-- src/modules/doing/repositories/TaskRepo.ts | 268 +++++------------- src/modules/doing/repositories/TimeRepo.ts | 24 +- 13 files changed, 292 insertions(+), 532 deletions(-) diff --git a/src/modules/doing/repositories/ActionRepo.ts b/src/modules/doing/repositories/ActionRepo.ts index 2a7f803b..ce7c6f4b 100644 --- a/src/modules/doing/repositories/ActionRepo.ts +++ b/src/modules/doing/repositories/ActionRepo.ts @@ -1,20 +1,14 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; -import { Action } from "../models/index.js"; - -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, and } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { actions } from "../../../db/schema/doing.js"; @injectable() -export class ActionRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "actions", - hasSoftDelete: false, - columns: ["automationId", "actionType", "actionData"] - }; - } +export class ActionRepo extends DrizzleRepo { + protected readonly table = actions; + protected readonly moduleName = "doing"; public loadForAutomation(churchId: string, automationId: string) { - return TypedDB.query("SELECT * FROM actions WHERE automationId=? AND churchId=?;", [automationId, churchId]); + return this.db.select().from(actions).where(and(eq(actions.automationId, automationId), eq(actions.churchId, churchId))); } } diff --git a/src/modules/doing/repositories/AssignmentRepo.ts b/src/modules/doing/repositories/AssignmentRepo.ts index fc43cac5..b378ecc9 100644 --- a/src/modules/doing/repositories/AssignmentRepo.ts +++ b/src/modules/doing/repositories/AssignmentRepo.ts @@ -1,80 +1,95 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; -import { Assignment } from "../models/index.js"; - -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, and, inArray, sql } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { assignments, positions, plans } from "../../../db/schema/doing.js"; @injectable() -export class AssignmentRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "assignments", - hasSoftDelete: false, - columns: ["positionId", "personId", "status", "notified"] - }; - } +export class AssignmentRepo extends DrizzleRepo { + protected readonly table = assignments; + protected readonly moduleName = "doing"; public deleteByPlanId(churchId: string, planId: string) { - return TypedDB.query("DELETE FROM assignments WHERE churchId=? and positionId IN (SELECT id from positions WHERE planId=?);", [churchId, planId]); + const positionIds = this.db.select({ id: positions.id }).from(positions).where(eq(positions.planId, planId)); + return this.db.delete(assignments).where(and(eq(assignments.churchId, churchId), inArray(assignments.positionId, positionIds))); } - public loadByPlanId(churchId: string, planId: string) { - return TypedDB.query("SELECT a.* FROM assignments a INNER JOIN positions p on p.id=a.positionId WHERE a.churchId=? AND p.planId=?;", [churchId, planId]); + public async loadByPlanId(churchId: string, planId: string): Promise { + const result = await this.db.select({ assignments }) + .from(assignments) + .innerJoin(positions, eq(positions.id, assignments.positionId)) + .where(and(eq(assignments.churchId, churchId), eq(positions.planId, planId))); + return result.map(r => r.assignments); } - public loadByPlanIds(churchId: string, planIds: string[]) { - return TypedDB.query("SELECT a.* FROM assignments a INNER JOIN positions p on p.id=a.positionId WHERE a.churchId=? AND p.planId IN (?);", [churchId, planIds]); + public async loadByPlanIds(churchId: string, planIds: string[]) { + const result = await this.db.select({ assignments }) + .from(assignments) + .innerJoin(positions, eq(positions.id, assignments.positionId)) + .where(and(eq(assignments.churchId, churchId), inArray(positions.planId, planIds))); + return result.map(r => r.assignments); } - public loadLastServed(churchId: string) { - const sql = - "select a.personId, max(pl.serviceDate) as serviceDate" + - " from assignments a" + - " inner join positions p on p.id = a.positionId" + - " inner join plans pl on pl.id = p.planId" + - " where a.churchId=?" + - " group by a.personId" + - " order by max(pl.serviceDate)"; - return TypedDB.query(sql, [churchId]); + public async loadLastServed(churchId: string) { + return this.db.select({ + personId: assignments.personId, + serviceDate: sql`max(${plans.serviceDate})`.as("serviceDate") + }) + .from(assignments) + .innerJoin(positions, eq(positions.id, assignments.positionId)) + .innerJoin(plans, eq(plans.id, positions.planId)) + .where(eq(assignments.churchId, churchId)) + .groupBy(assignments.personId) + .orderBy(sql`max(${plans.serviceDate})`); } public loadByByPersonId(churchId: string, personId: string) { - return TypedDB.query("SELECT * FROM assignments WHERE churchId=? AND personId=?;", [churchId, personId]); + return this.db.select().from(assignments).where(and(eq(assignments.churchId, churchId), eq(assignments.personId, personId))); } - public loadUnconfirmedByServiceDateRange(churchId?: string) { - let sql = - "SELECT a.*, pl.serviceDate, pl.name as planName" + - " FROM assignments a" + - " INNER JOIN positions p ON p.id = a.positionId" + - " INNER JOIN plans pl ON pl.id = p.planId" + - " WHERE a.status = 'Unconfirmed'" + - " AND pl.serviceDate >= DATE_ADD(CURDATE(), INTERVAL 2 DAY)" + - " AND pl.serviceDate < DATE_ADD(CURDATE(), INTERVAL 3 DAY)"; - - const params: any[] = []; - if (churchId) { - sql += " AND a.churchId = ?"; - params.push(churchId); - } - - return TypedDB.query(sql, params); + public async loadUnconfirmedByServiceDateRange(churchId?: string) { + const baseQuery = this.db.select({ + assignments, + serviceDate: plans.serviceDate, + planName: plans.name + }) + .from(assignments) + .innerJoin(positions, eq(positions.id, assignments.positionId)) + .innerJoin(plans, eq(plans.id, positions.planId)) + .where( + churchId + ? and( + eq(assignments.status, "Unconfirmed"), + sql`${plans.serviceDate} >= DATE_ADD(CURDATE(), INTERVAL 2 DAY)`, + sql`${plans.serviceDate} < DATE_ADD(CURDATE(), INTERVAL 3 DAY)`, + eq(assignments.churchId, churchId) + ) + : and( + eq(assignments.status, "Unconfirmed"), + sql`${plans.serviceDate} >= DATE_ADD(CURDATE(), INTERVAL 2 DAY)`, + sql`${plans.serviceDate} < DATE_ADD(CURDATE(), INTERVAL 3 DAY)` + ) + ); + const result = await baseQuery; + return result.map(r => ({ ...r.assignments, serviceDate: r.serviceDate, planName: r.planName })); } - public countByPositionId(churchId: string, positionId: string) { - return TypedDB.queryOne( - "SELECT COUNT(*) as cnt FROM assignments WHERE churchId=? AND positionId=? AND status IN ('Accepted','Unconfirmed')", - [churchId, positionId] - ); + public async countByPositionId(churchId: string, positionId: string) { + const result = await this.db.select({ cnt: sql`COUNT(*)` }) + .from(assignments) + .where(and(eq(assignments.churchId, churchId), eq(assignments.positionId, positionId), inArray(assignments.status, ["Accepted", "Unconfirmed"]))); + return result[0]; } - public loadByServiceDate(churchId: string, serviceDate: Date, excludePlanId?: string) { - let sql = "SELECT a.* FROM assignments a" + " INNER JOIN positions p ON p.id = a.positionId" + " INNER JOIN plans pl ON pl.id = p.planId" + " WHERE a.churchId = ? AND DATE(pl.serviceDate) = DATE(?)"; - const params: any[] = [churchId, serviceDate]; - if (excludePlanId) { - sql += " AND pl.id != ?"; - params.push(excludePlanId); - } - return TypedDB.query(sql, params); + public async loadByServiceDate(churchId: string, serviceDate: Date, excludePlanId?: string) { + const condition = excludePlanId + ? and(eq(assignments.churchId, churchId), sql`DATE(${plans.serviceDate}) = DATE(${serviceDate})`, sql`${plans.id} != ${excludePlanId}`) + : and(eq(assignments.churchId, churchId), sql`DATE(${plans.serviceDate}) = DATE(${serviceDate})`); + + const result = await this.db.select({ assignments }) + .from(assignments) + .innerJoin(positions, eq(positions.id, assignments.positionId)) + .innerJoin(plans, eq(plans.id, positions.planId)) + .where(condition); + return result.map(r => r.assignments); } } diff --git a/src/modules/doing/repositories/AutomationRepo.ts b/src/modules/doing/repositories/AutomationRepo.ts index f05e13ce..35bb0bfd 100644 --- a/src/modules/doing/repositories/AutomationRepo.ts +++ b/src/modules/doing/repositories/AutomationRepo.ts @@ -1,21 +1,18 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; -import { Automation } from "../models/index.js"; - -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, asc } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { automations } from "../../../db/schema/doing.js"; @injectable() -export class AutomationRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "automations", - hasSoftDelete: false, - defaultOrderBy: "title", - columns: ["title", "recurs", "active"] - }; +export class AutomationRepo extends DrizzleRepo { + protected readonly table = automations; + protected readonly moduleName = "doing"; + + public loadAll(churchId: string) { + return this.db.select().from(automations).where(eq(automations.churchId, churchId)).orderBy(asc(automations.title)); } public loadAllChurches() { - return TypedDB.query("SELECT * FROM automations;", []); + return this.db.select().from(automations); } } diff --git a/src/modules/doing/repositories/BlockoutDateRepo.ts b/src/modules/doing/repositories/BlockoutDateRepo.ts index 2b8db61c..7aa51ce4 100644 --- a/src/modules/doing/repositories/BlockoutDateRepo.ts +++ b/src/modules/doing/repositories/BlockoutDateRepo.ts @@ -1,51 +1,43 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; +import { eq, and, inArray, gt, sql } from "drizzle-orm"; +import { UniqueIdHelper } from "@churchapps/apihelper"; import { DateHelper } from "../../../shared/helpers/DateHelper.js"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { blockoutDates } from "../../../db/schema/doing.js"; import { BlockoutDate } from "../models/index.js"; -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; - @injectable() -export class BlockoutDateRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "blockoutDates", - hasSoftDelete: false, - columns: ["personId", "startDate", "endDate"] - }; - } +export class BlockoutDateRepo extends DrizzleRepo { + protected readonly table = blockoutDates; + protected readonly moduleName = "doing"; - public save(blockoutDate: BlockoutDate) { - // Convert date-only fields before saving - const processedData = { ...blockoutDate }; + public override async save(model: BlockoutDate) { + const processedData = { ...model } as any; if (processedData.startDate) { - (processedData as any).startDate = DateHelper.toMysqlDateOnly(processedData.startDate); + processedData.startDate = DateHelper.toMysqlDateOnly(processedData.startDate); } if (processedData.endDate) { - (processedData as any).endDate = DateHelper.toMysqlDateOnly(processedData.endDate); + processedData.endDate = DateHelper.toMysqlDateOnly(processedData.endDate); + } + + if (processedData.id) { + await this.db.update(blockoutDates).set(processedData).where(and(eq(blockoutDates.id, processedData.id), eq(blockoutDates.churchId, processedData.churchId))); + } else { + processedData.id = UniqueIdHelper.shortId(); + await this.db.insert(blockoutDates).values(processedData); } - return super.save(processedData); + return processedData as BlockoutDate; } public loadByIds(churchId: string, ids: string[]) { - return TypedDB.query("SELECT * FROM blockoutDates WHERE churchId=? and id in (?);", [churchId, ids]); + return this.db.select().from(blockoutDates).where(and(eq(blockoutDates.churchId, churchId), inArray(blockoutDates.id, ids))); } public loadForPerson(churchId: string, personId: string) { - return TypedDB.query("SELECT * FROM blockoutDates WHERE churchId=? and personId=?;", [churchId, personId]); + return this.db.select().from(blockoutDates).where(and(eq(blockoutDates.churchId, churchId), eq(blockoutDates.personId, personId))); } public loadUpcoming(churchId: string) { - return TypedDB.query("SELECT * FROM blockoutDates WHERE churchId=? AND endDate>NOW();", [churchId]); - } - - protected rowToModel(row: any): BlockoutDate { - return { - id: row.id, - churchId: row.churchId, - personId: row.personId, - startDate: row.startDate, - endDate: row.endDate - }; + return this.db.select().from(blockoutDates).where(and(eq(blockoutDates.churchId, churchId), gt(blockoutDates.endDate, sql`NOW()`))); } } diff --git a/src/modules/doing/repositories/ConditionRepo.ts b/src/modules/doing/repositories/ConditionRepo.ts index 069c1956..519fa0a7 100644 --- a/src/modules/doing/repositories/ConditionRepo.ts +++ b/src/modules/doing/repositories/ConditionRepo.ts @@ -1,20 +1,15 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; -import { Condition } from "../models/index.js"; - -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, and, inArray } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { conditions, conjunctions } from "../../../db/schema/doing.js"; @injectable() -export class ConditionRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "conditions", - hasSoftDelete: false, - columns: ["conjunctionId", "field", "fieldData", "operator", "value", "label"] - }; - } +export class ConditionRepo extends DrizzleRepo { + protected readonly table = conditions; + protected readonly moduleName = "doing"; - public loadForAutomation(churchId: string, automationId: string) { - return TypedDB.query("SELECT * FROM conditions WHERE conjunctionId IN (SELECT id FROM conjunctions WHERE automationId=?) AND churchId=?;", [automationId, churchId]); + public async loadForAutomation(churchId: string, automationId: string) { + const conjunctionIds = this.db.select({ id: conjunctions.id }).from(conjunctions).where(eq(conjunctions.automationId, automationId)); + return this.db.select().from(conditions).where(and(inArray(conditions.conjunctionId, conjunctionIds), eq(conditions.churchId, churchId))); } } diff --git a/src/modules/doing/repositories/ConjunctionRepo.ts b/src/modules/doing/repositories/ConjunctionRepo.ts index 1b97b855..9389a071 100644 --- a/src/modules/doing/repositories/ConjunctionRepo.ts +++ b/src/modules/doing/repositories/ConjunctionRepo.ts @@ -1,19 +1,14 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; -import { Conjunction } from "../models/index.js"; -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, and } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { conjunctions } from "../../../db/schema/doing.js"; @injectable() -export class ConjunctionRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "conjunctions", - hasSoftDelete: false, - columns: ["automationId", "parentId", "groupType"] - }; - } +export class ConjunctionRepo extends DrizzleRepo { + protected readonly table = conjunctions; + protected readonly moduleName = "doing"; public loadForAutomation(churchId: string, automationId: string) { - return TypedDB.query("SELECT * FROM conjunctions WHERE automationId=? AND churchId=?;", [automationId, churchId]); + return this.db.select().from(conjunctions).where(and(eq(conjunctions.automationId, automationId), eq(conjunctions.churchId, churchId))); } } diff --git a/src/modules/doing/repositories/ContentProviderAuthRepo.ts b/src/modules/doing/repositories/ContentProviderAuthRepo.ts index 84e735c6..e0b18896 100644 --- a/src/modules/doing/repositories/ContentProviderAuthRepo.ts +++ b/src/modules/doing/repositories/ContentProviderAuthRepo.ts @@ -1,41 +1,24 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; -import { ContentProviderAuth } from "../models/index.js"; -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, and, inArray } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { contentProviderAuths } from "../../../db/schema/doing.js"; @injectable() -export class ContentProviderAuthRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "contentProviderAuths", - hasSoftDelete: false, - columns: ["ministryId", "providerId", "accessToken", "refreshToken", "tokenType", "expiresAt", "scope"] - }; - } +export class ContentProviderAuthRepo extends DrizzleRepo { + protected readonly table = contentProviderAuths; + protected readonly moduleName = "doing"; public loadByIds(churchId: string, ids: string[]) { - return TypedDB.query("SELECT * FROM contentProviderAuths WHERE churchId=? and id in (?);", [churchId, ids]); + return this.db.select().from(contentProviderAuths).where(and(eq(contentProviderAuths.churchId, churchId), inArray(contentProviderAuths.id, ids))); } public loadByMinistry(churchId: string, ministryId: string) { - return TypedDB.query("SELECT * FROM contentProviderAuths WHERE churchId=? AND ministryId=?;", [churchId, ministryId]); + return this.db.select().from(contentProviderAuths).where(and(eq(contentProviderAuths.churchId, churchId), eq(contentProviderAuths.ministryId, ministryId))); } public loadByMinistryAndProvider(churchId: string, ministryId: string, providerId: string) { - return TypedDB.queryOne("SELECT * FROM contentProviderAuths WHERE churchId=? AND ministryId=? AND providerId=?;", [churchId, ministryId, providerId]); - } - - protected rowToModel(row: any): ContentProviderAuth { - return { - id: row.id, - churchId: row.churchId, - ministryId: row.ministryId, - providerId: row.providerId, - accessToken: row.accessToken, - refreshToken: row.refreshToken, - tokenType: row.tokenType, - expiresAt: row.expiresAt ? new Date(row.expiresAt) : undefined, - scope: row.scope - }; + return this.db.select().from(contentProviderAuths) + .where(and(eq(contentProviderAuths.churchId, churchId), eq(contentProviderAuths.ministryId, ministryId), eq(contentProviderAuths.providerId, providerId))) + .then(r => r[0] ?? null); } } diff --git a/src/modules/doing/repositories/PlanItemRepo.ts b/src/modules/doing/repositories/PlanItemRepo.ts index 238dc545..655cbbe6 100644 --- a/src/modules/doing/repositories/PlanItemRepo.ts +++ b/src/modules/doing/repositories/PlanItemRepo.ts @@ -1,29 +1,22 @@ -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; import { injectable } from "inversify"; -import { PlanItem } from "../models/index.js"; -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, and, inArray, asc } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { planItems } from "../../../db/schema/doing.js"; @injectable() -export class PlanItemRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "planItems", - hasSoftDelete: false, - columns: [ - "planId", "parentId", "sort", "itemType", "relatedId", "label", "description", "seconds", "link", "providerId", "providerPath", "providerContentPath", "thumbnailUrl" - ] - }; - } +export class PlanItemRepo extends DrizzleRepo { + protected readonly table = planItems; + protected readonly moduleName = "doing"; public deleteByPlanId(churchId: string, planId: string) { - return TypedDB.query("DELETE FROM planItems WHERE churchId=? and planId=?;", [churchId, planId]); + return this.db.delete(planItems).where(and(eq(planItems.churchId, churchId), eq(planItems.planId, planId))); } public loadByIds(churchId: string, ids: string[]) { - return TypedDB.query("SELECT * FROM planItems WHERE churchId=? and id in (?);", [churchId, ids]); + return this.db.select().from(planItems).where(and(eq(planItems.churchId, churchId), inArray(planItems.id, ids))); } public loadForPlan(churchId: string, planId: string) { - return TypedDB.query("SELECT * FROM planItems WHERE churchId=? and planId=? ORDER BY sort", [churchId, planId]); + return this.db.select().from(planItems).where(and(eq(planItems.churchId, churchId), eq(planItems.planId, planId))).orderBy(asc(planItems.sort)); } } diff --git a/src/modules/doing/repositories/PlanRepo.ts b/src/modules/doing/repositories/PlanRepo.ts index 5759d810..8a440d89 100644 --- a/src/modules/doing/repositories/PlanRepo.ts +++ b/src/modules/doing/repositories/PlanRepo.ts @@ -1,106 +1,59 @@ -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; import { injectable } from "inversify"; +import { eq, and, inArray, desc, asc, gte, between, sql } from "drizzle-orm"; +import { UniqueIdHelper } from "@churchapps/apihelper"; +import { DateHelper } from "../../../shared/helpers/DateHelper.js"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { plans, positions } from "../../../db/schema/doing.js"; import { Plan } from "../models/index.js"; -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; @injectable() -export class PlanRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "plans", - hasSoftDelete: false, - columns: [ - "ministryId", - "planTypeId", - "name", - "serviceDate", - "notes", - "serviceOrder", - "contentType", - "contentId", - "providerId", - "providerPlanId", - "providerPlanName", - "signupDeadlineHours", - "showVolunteerNames" - ] - }; - } - - protected async create(plan: Plan): Promise { - if (!plan.id) plan.id = this.createId(); +export class PlanRepo extends DrizzleRepo { + protected readonly table = plans; + protected readonly moduleName = "doing"; - const sql = "INSERT INTO plans (id, churchId, ministryId, planTypeId, name, serviceDate, notes, serviceOrder, contentType, contentId, providerId, providerPlanId, providerPlanName, signupDeadlineHours, showVolunteerNames) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; - const params = [ - plan.id, - plan.churchId, - plan.ministryId, - plan.planTypeId, - plan.name, - plan.serviceDate?.toISOString().split("T")[0] || new Date().toISOString().split("T")[0], - plan.notes, - plan.serviceOrder, - plan.contentType, - plan.contentId, - plan.providerId, - plan.providerPlanId, - plan.providerPlanName, - plan.signupDeadlineHours, - plan.showVolunteerNames - ]; - await TypedDB.query(sql, params); - return plan; - } + public override async save(model: Plan) { + const data = { ...model } as any; + data.serviceDate = DateHelper.toMysqlDateOnly(data.serviceDate) || new Date().toISOString().split("T")[0]; - protected async update(plan: Plan): Promise { - const sql = "UPDATE plans SET ministryId=?, planTypeId=?, name=?, serviceDate=?, notes=?, serviceOrder=?, contentType=?, contentId=?, providerId=?, providerPlanId=?, providerPlanName=?, signupDeadlineHours=?, showVolunteerNames=? WHERE id=? and churchId=?"; - const params = [ - plan.ministryId, - plan.planTypeId, - plan.name, - plan.serviceDate?.toISOString().split("T")[0] || new Date().toISOString().split("T")[0], - plan.notes, - plan.serviceOrder, - plan.contentType, - plan.contentId, - plan.providerId, - plan.providerPlanId, - plan.providerPlanName, - plan.signupDeadlineHours, - plan.showVolunteerNames, - plan.id, - plan.churchId - ]; - await TypedDB.query(sql, params); - return plan; + if (data.id) { + await this.db.update(plans).set(data).where(and(eq(plans.id, data.id), eq(plans.churchId, data.churchId))); + } else { + data.id = UniqueIdHelper.shortId(); + await this.db.insert(plans).values(data); + } + return data as Plan; } public loadByIds(churchId: string, ids: string[]) { - return TypedDB.query("SELECT * FROM plans WHERE churchId=? and id in (?);", [churchId, ids]); + return this.db.select().from(plans).where(and(eq(plans.churchId, churchId), inArray(plans.id, ids))); } public load7Days(churchId: string) { - return TypedDB.query("SELECT * FROM plans WHERE churchId=? AND serviceDate BETWEEN CURDATE() AND (CURDATE() + INTERVAL 7 DAY) order by serviceDate desc;", [churchId]); + return this.db.select().from(plans) + .where(and(eq(plans.churchId, churchId), between(plans.serviceDate, sql`CURDATE()`, sql`CURDATE() + INTERVAL 7 DAY`))) + .orderBy(desc(plans.serviceDate)); } public loadByPlanTypeId(churchId: string, planTypeId: string) { - return TypedDB.query("SELECT * FROM plans WHERE churchId=? AND planTypeId=? order by serviceDate desc;", [churchId, planTypeId]); + return this.db.select().from(plans) + .where(and(eq(plans.churchId, churchId), eq(plans.planTypeId, planTypeId))) + .orderBy(desc(plans.serviceDate)); } public loadCurrentByPlanTypeId(planTypeId: string) { - return TypedDB.queryOne( - "SELECT * FROM plans WHERE planTypeId=? AND serviceDate>=CURDATE() ORDER by serviceDate LIMIT 1", - [planTypeId] - ); - } - - public loadSignupPlans(churchId: string) { - return TypedDB.query( - "SELECT DISTINCT p.* FROM plans p" - + " INNER JOIN positions pos ON pos.planId = p.id AND pos.churchId = p.churchId" - + " WHERE p.churchId = ? AND pos.allowSelfSignup = 1 AND p.serviceDate >= CURDATE()" - + " ORDER BY p.serviceDate ASC", - [churchId] - ); + return this.db.select().from(plans) + .where(and(eq(plans.planTypeId, planTypeId), gte(plans.serviceDate, sql`CURDATE()`))) + .orderBy(asc(plans.serviceDate)) + .limit(1) + .then(r => r[0] ?? null); + } + + public async loadSignupPlans(churchId: string) { + const result = await this.db.selectDistinct({ plans }) + .from(plans) + .innerJoin(positions, and(eq(positions.planId, plans.id), eq(positions.churchId, plans.churchId))) + .where(and(eq(plans.churchId, churchId), eq(positions.allowSelfSignup, true), gte(plans.serviceDate, sql`CURDATE()`))) + .orderBy(asc(plans.serviceDate)); + return result.map(r => r.plans); } } diff --git a/src/modules/doing/repositories/PlanTypeRepo.ts b/src/modules/doing/repositories/PlanTypeRepo.ts index 7345fb90..f3eaf2bc 100644 --- a/src/modules/doing/repositories/PlanTypeRepo.ts +++ b/src/modules/doing/repositories/PlanTypeRepo.ts @@ -1,33 +1,18 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; -import { PlanType } from "../models/index.js"; - -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, and, inArray } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { planTypes } from "../../../db/schema/doing.js"; @injectable() -export class PlanTypeRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "planTypes", - hasSoftDelete: false, - columns: ["ministryId", "name"] - }; - } +export class PlanTypeRepo extends DrizzleRepo { + protected readonly table = planTypes; + protected readonly moduleName = "doing"; public loadByIds(churchId: string, ids: string[]) { - return TypedDB.query("SELECT * FROM planTypes WHERE churchId=? and id in (?);", [churchId, ids]); + return this.db.select().from(planTypes).where(and(eq(planTypes.churchId, churchId), inArray(planTypes.id, ids))); } public loadByMinistryId(churchId: string, ministryId: string) { - return TypedDB.query("SELECT * FROM planTypes WHERE churchId=? AND ministryId=?;", [churchId, ministryId]); - } - - protected rowToModel(row: any): PlanType { - return { - id: row.id, - churchId: row.churchId, - ministryId: row.ministryId, - name: row.name - }; + return this.db.select().from(planTypes).where(and(eq(planTypes.churchId, churchId), eq(planTypes.ministryId, ministryId))); } } diff --git a/src/modules/doing/repositories/PositionRepo.ts b/src/modules/doing/repositories/PositionRepo.ts index 5c6958eb..451314c0 100644 --- a/src/modules/doing/repositories/PositionRepo.ts +++ b/src/modules/doing/repositories/PositionRepo.ts @@ -1,50 +1,32 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; -import { Position } from "../models/index.js"; - -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, and, inArray, asc } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { positions } from "../../../db/schema/doing.js"; @injectable() -export class PositionRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "positions", - hasSoftDelete: false, - columns: ["planId", "categoryName", "name", "count", "groupId", "allowSelfSignup", "description"] - }; - } +export class PositionRepo extends DrizzleRepo { + protected readonly table = positions; + protected readonly moduleName = "doing"; public deleteByPlanId(churchId: string, planId: string) { - return TypedDB.query("DELETE FROM positions WHERE churchId=? and planId=?;", [churchId, planId]); + return this.db.delete(positions).where(and(eq(positions.churchId, churchId), eq(positions.planId, planId))); } public loadByIds(churchId: string, ids: string[]) { - return TypedDB.query("SELECT * FROM positions WHERE churchId=? and id in (?);", [churchId, ids]); + return this.db.select().from(positions).where(and(eq(positions.churchId, churchId), inArray(positions.id, ids))); } public loadByPlanId(churchId: string, planId: string) { - return TypedDB.query("SELECT * FROM positions WHERE churchId=? AND planId=? ORDER BY categoryName, name;", [churchId, planId]); + return this.db.select().from(positions).where(and(eq(positions.churchId, churchId), eq(positions.planId, planId))).orderBy(asc(positions.categoryName), asc(positions.name)); } public loadByPlanIds(churchId: string, planIds: string[]) { - return TypedDB.query("SELECT * FROM positions WHERE churchId=? AND planId in (?);", [churchId, planIds]); + return this.db.select().from(positions).where(and(eq(positions.churchId, churchId), inArray(positions.planId, planIds))); } public loadSignupByPlanId(churchId: string, planId: string) { - return TypedDB.query("SELECT * FROM positions WHERE churchId=? AND planId=? AND allowSelfSignup=1 ORDER BY categoryName, name;", [churchId, planId]); - } - - protected rowToModel(row: any): Position { - return { - id: row.id, - churchId: row.churchId, - planId: row.planId, - categoryName: row.categoryName, - name: row.name, - count: row.count, - groupId: row.groupId, - allowSelfSignup: row.allowSelfSignup, - description: row.description - }; + return this.db.select().from(positions) + .where(and(eq(positions.churchId, churchId), eq(positions.planId, planId), eq(positions.allowSelfSignup, true))) + .orderBy(asc(positions.categoryName), asc(positions.name)); } } diff --git a/src/modules/doing/repositories/TaskRepo.ts b/src/modules/doing/repositories/TaskRepo.ts index 1b847784..accfd93c 100644 --- a/src/modules/doing/repositories/TaskRepo.ts +++ b/src/modules/doing/repositories/TaskRepo.ts @@ -1,231 +1,113 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; +import { eq, and, inArray, gt, or, asc, sql } from "drizzle-orm"; +import { UniqueIdHelper } from "@churchapps/apihelper"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { tasks } from "../../../db/schema/doing.js"; import { Task } from "../models/index.js"; -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; @injectable() -export class TaskRepo extends ConfiguredRepo { - protected get repoConfig(): RepoConfig { - return { - tableName: "tasks", - hasSoftDelete: false, - insertColumns: [ - "taskNumber", - "taskType", - "dateCreated", - "dateClosed", - "associatedWithType", - "associatedWithId", - "associatedWithLabel", - "createdByType", - "createdById", - "createdByLabel", - "assignedToType", - "assignedToId", - "assignedToLabel", - "title", - "status", - "automationId", - "conversationId", - "data" - ], - updateColumns: [ - "taskType", - "dateCreated", - "dateClosed", - "associatedWithType", - "associatedWithId", - "associatedWithLabel", - "createdByType", - "createdById", - "createdByLabel", - "assignedToType", - "assignedToId", - "assignedToLabel", - "title", - "status", - "automationId", - "conversationId", - "data" - ] - }; - } - - protected async create(task: Task): Promise { - if (!task.id) task.id = this.createId(); - const taskNumber = await this.loadNextTaskNumber(task.churchId || ""); // NOTE - This is problematic if saving multiple records asyncronously. Be sure to await each call - - const sql = - "INSERT INTO tasks (id, churchId, taskNumber, taskType, dateCreated, dateClosed, associatedWithType, associatedWithId, associatedWithLabel, createdByType, createdById, createdByLabel, assignedToType, assignedToId, assignedToLabel, title, status, automationId, conversationId, data) VALUES (?, ?, ?, ?, now(), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; - const params = [ - task.id, - task.churchId, - taskNumber, - task.taskType, - task.dateClosed, - task.associatedWithType, - task.associatedWithId, - task.associatedWithLabel, - task.createdByType, - task.createdById, - task.createdByLabel, - task.assignedToType, - task.assignedToId, - task.assignedToLabel, - task.title, - task.status, - task.automationId, - task.conversationId, - task.data - ]; - await TypedDB.query(sql, params); - return task; +export class TaskRepo extends DrizzleRepo { + protected readonly table = tasks; + protected readonly moduleName = "doing"; + + public override async save(model: Task) { + if (model.id) { + await this.db.update(tasks).set(model).where(and(eq(tasks.id, model.id), eq(tasks.churchId, model.churchId))); + } else { + model.id = UniqueIdHelper.shortId(); + const taskNumber = await this.loadNextTaskNumber(model.churchId || ""); + const data = { ...model, taskNumber, dateCreated: new Date() } as any; + await this.db.insert(tasks).values(data); + return data as Task; + } + return model; } - protected async update(task: Task): Promise { - const sql = - "UPDATE tasks SET taskType=?, dateCreated=?, dateClosed=?, associatedWithType=?, associatedWithId=?, associatedWithLabel=?, createdByType=?, createdById=?, createdByLabel=?, assignedToType=?, assignedToId=?, assignedToLabel=?, title=?, status=?, automationId=?, conversationId=?, data=? WHERE id=? and churchId=?"; - const params = [ - task.taskType, - task.dateCreated, - task.dateClosed, - task.associatedWithType, - task.associatedWithId, - task.associatedWithLabel, - task.createdByType, - task.createdById, - task.createdByLabel, - task.assignedToType, - task.assignedToId, - task.assignedToLabel, - task.title, - task.status, - task.automationId, - task.conversationId, - task.data, - task.id, - task.churchId - ]; - await TypedDB.query(sql, params); - return task; + public override load(churchId: string, id: string) { + return this.db.select().from(tasks).where(and(eq(tasks.id, id), eq(tasks.churchId, churchId))).orderBy(asc(tasks.taskNumber)).then(r => r[0] ?? null); } public async loadTimeline(churchId: string, personId: string, taskIds: string[]) { - let sql = - "select *, 'task' as postType, id as postId from tasks" + - " where churchId=? AND (" + - " status='Open' and (" + - " (associatedWithType='person' and associatedWithId=?)" + - " OR" + - " (createdByType='person' and createdById=?)" + - " OR" + - " (assignedToType='person' and assignedToId=?)" + - " )" + - ")"; - const params: unknown[] = [churchId, personId, personId, personId]; + const personCondition = and( + eq(tasks.churchId, churchId), + eq(tasks.status, "Open"), + or( + and(eq(tasks.associatedWithType, "person"), eq(tasks.associatedWithId, personId)), + and(eq(tasks.createdByType, "person"), eq(tasks.createdById, personId)), + and(eq(tasks.assignedToType, "person"), eq(tasks.assignedToId, personId)) + ) + ); + if (taskIds.length > 0) { - sql += " OR id IN (?)"; - params.push(taskIds); + return this.db.select().from(tasks).where(or(personCondition, inArray(tasks.id, taskIds))); } - const result = await TypedDB.query(sql, params); - return result; + return this.db.select().from(tasks).where(personCondition); } public async loadByAutomationIdContent(churchId: string, automationId: string, recurs: string, associatedWithType: string, associatedWithIds: string[]) { - let result: any[] = []; + let threshold: Date | null = null; switch (recurs) { case "yearly": - result = (await this.loadByAutomationIdContentYearly(churchId, automationId, associatedWithType, associatedWithIds)) as any[]; + threshold = new Date(); + threshold.setFullYear(threshold.getFullYear() - 1); break; case "monthly": - result = (await this.loadByAutomationIdContentMonthly(churchId, automationId, associatedWithType, associatedWithIds)) as any[]; + threshold = new Date(); + threshold.setMonth(threshold.getMonth() - 1); break; case "weekly": - result = (await this.loadByAutomationIdContentWeekly(churchId, automationId, associatedWithType, associatedWithIds)) as any[]; - break; - default: - result = (await this.loadByAutomationIdContentNoRepeat(churchId, automationId, associatedWithType, associatedWithIds)) as any[]; + threshold = new Date(); + threshold.setDate(threshold.getDate() - 7); break; } - return result; - } - - private async loadByAutomationIdContentNoRepeat(churchId: string, automationId: string, associatedWithType: string, associatedWithIds: string[]) { - const result = await TypedDB.query("SELECT * FROM tasks WHERE churchId=? AND automationId=? AND associatedWithType=? AND associatedWithId IN (?) order by taskNumber;", [ - churchId, - automationId, - associatedWithType, - associatedWithIds - ]); - return result; - } - private async loadByAutomationIdContentYearly(churchId: string, automationId: string, associatedWithType: string, associatedWithIds: string[]) { - const threshold = new Date(); - threshold.setFullYear(threshold.getFullYear() - 1); - const result = await TypedDB.query("SELECT * FROM tasks WHERE churchId=? AND automationId=? AND associatedWithType=? AND associatedWithId IN (?) and dateCreated>? order by taskNumber;", [ - churchId, - automationId, - associatedWithType, - associatedWithIds, - threshold - ]); - return result; - } - - private async loadByAutomationIdContentMonthly(churchId: string, automationId: string, associatedWithType: string, associatedWithIds: string[]) { - const threshold = new Date(); - threshold.setMonth(threshold.getMonth() - 1); - const result = await TypedDB.query("SELECT * FROM tasks WHERE churchId=? AND automationId=? AND associatedWithType=? AND associatedWithId IN (?) and dateCreated>? order by taskNumber;", [ - churchId, - automationId, - associatedWithType, - associatedWithIds, - threshold - ]); - return result; - } + const baseCondition = and( + eq(tasks.churchId, churchId), + eq(tasks.automationId, automationId), + eq(tasks.associatedWithType, associatedWithType), + inArray(tasks.associatedWithId, associatedWithIds) + ); - private async loadByAutomationIdContentWeekly(churchId: string, automationId: string, associatedWithType: string, associatedWithIds: string[]) { - const threshold = new Date(); - threshold.setDate(threshold.getDate() - 7); - const result = await TypedDB.query("SELECT * FROM tasks WHERE churchId=? AND automationId=? AND associatedWithType=? AND associatedWithId IN (?) and dateCreated>? order by taskNumber;", [churchId, automationId, associatedWithType, associatedWithIds, threshold]); - return result; + if (threshold) { + return this.db.select().from(tasks).where(and(baseCondition, gt(tasks.dateCreated, threshold))).orderBy(asc(tasks.taskNumber)); + } + return this.db.select().from(tasks).where(baseCondition).orderBy(asc(tasks.taskNumber)); } private async loadNextTaskNumber(churchId: string) { - const result = await TypedDB.queryOne("select max(ifnull(taskNumber, 0)) + 1 as taskNumber from tasks where churchId=?", [churchId]); - return (result as any).taskNumber; + const result = await this.db.select({ taskNumber: sql`max(ifnull(taskNumber, 0)) + 1` }).from(tasks).where(eq(tasks.churchId, churchId)); + return result[0]?.taskNumber ?? 1; } public loadForPerson(churchId: string, personId: string, status: string) { - return TypedDB.query("SELECT * FROM tasks WHERE churchId=? AND ((assignedToType='person' AND assignedToId=?) OR (createdByType='person' AND createdById=?)) and status=? order by taskNumber;", [ - churchId, - personId, - personId, - status - ]); + return this.db.select().from(tasks) + .where(and( + eq(tasks.churchId, churchId), + eq(tasks.status, status), + or( + and(eq(tasks.assignedToType, "person"), eq(tasks.assignedToId, personId)), + and(eq(tasks.createdByType, "person"), eq(tasks.createdById, personId)) + ) + )) + .orderBy(asc(tasks.taskNumber)); } public async loadForGroups(churchId: string, groupIds: string[], status: string) { if (groupIds.length === 0) return []; - else { - return TypedDB.query( - "SELECT * FROM tasks WHERE churchId=? AND ((assignedToType='group' AND assignedToId IN (?)) OR (createdByType='group' AND createdById IN (?))) AND status=? order by taskNumber;", - [churchId, groupIds, groupIds, status] - ); - } - } - - public async loadForDirectoryUpdate(churchId: string, personId: string) { - return TypedDB.query("SELECT * FROM tasks WHERE taskType='directoryUpdate' AND status='Open' AND churchId=? AND associatedWithId=?;", [churchId, personId]); - } - - public load(churchId: string, id: string) { - return TypedDB.queryOne("SELECT * FROM tasks WHERE id=? AND churchId=? order by taskNumber;", [id, churchId]); + return this.db.select().from(tasks) + .where(and( + eq(tasks.churchId, churchId), + eq(tasks.status, status), + or( + and(eq(tasks.assignedToType, "group"), inArray(tasks.assignedToId, groupIds)), + and(eq(tasks.createdByType, "group"), inArray(tasks.createdById, groupIds)) + ) + )) + .orderBy(asc(tasks.taskNumber)); } - public loadAll(churchId: string) { - return TypedDB.query("SELECT * FROM tasks WHERE churchId=?;", [churchId]); + public loadForDirectoryUpdate(churchId: string, personId: string) { + return this.db.select().from(tasks) + .where(and(eq(tasks.taskType, "directoryUpdate"), eq(tasks.status, "Open"), eq(tasks.churchId, churchId), eq(tasks.associatedWithId, personId))); } } diff --git a/src/modules/doing/repositories/TimeRepo.ts b/src/modules/doing/repositories/TimeRepo.ts index 96fc6425..e72af1d9 100644 --- a/src/modules/doing/repositories/TimeRepo.ts +++ b/src/modules/doing/repositories/TimeRepo.ts @@ -1,28 +1,22 @@ import { injectable } from "inversify"; -import { TypedDB } from "../../../shared/infrastructure/TypedDB.js"; -import { Time } from "../models/index.js"; - -import { ConfiguredRepo, RepoConfig } from "../../../shared/infrastructure/ConfiguredRepo.js"; +import { eq, and, inArray } from "drizzle-orm"; +import { DrizzleRepo } from "../../../shared/infrastructure/DrizzleRepo.js"; +import { times } from "../../../db/schema/doing.js"; @injectable() -export class TimeRepo extends ConfiguredRepo