[Typescript] Migrate worker to typescript

This commit is contained in:
Aaro Koivukangas
2026-09-30 15:45:58 +03:00
parent 4d984e2596
commit 5c0756e2bc
69 changed files with 1369 additions and 367 deletions
+10 -3
View File
@@ -251,9 +251,10 @@
}
},
{
"files": ["updater/sis-updater-scheduler/src/**/*.ts"],
"files": ["updater/sis-updater-scheduler/src/**/*.ts", "updater/sis-updater-worker/src/**/*.ts"],
"rules": {
"typescript/no-misused-promises": "off"
"typescript/no-misused-promises": "off",
"typescript/consistent-return": "off"
}
},
{
@@ -359,6 +360,12 @@
"no-console": "off"
}
},
{
"files": ["updater/sis-updater-worker/tests/**/*.ts"],
"rules": {
"typescript/unbound-method": "off"
}
},
{
"files": [
"services/backend/**/*.js",
@@ -378,7 +385,7 @@
}
},
{
"files": ["services/backend/**/*.{js,ts}", "updater/**/*.js"],
"files": ["services/backend/**/*.{js,ts}", "updater/**/*.{js,ts,mts}"],
"rules": {
"no-await-in-loop": "off",
"no-promise-executor-return": "off",
+1 -1
View File
@@ -56,7 +56,7 @@ If you need to **revert a migration locally**, follow these steps:
1. Locate the `migrator.up()` function in the code:
- For `user-db` and `kone-db`: [`services/backend/src/database/connection.ts`](../services/backend/src/database/connection.ts)
- For `sis-db`: [`updater/sis-updater-worker/src/db/connection.js`](../updater/sis-updater-worker/src/db/connection.js)
- For `sis-db`: [`updater/sis-updater-worker/src/db/connection.ts`](../updater/sis-updater-worker/src/db/connection.ts)
1. Change `migrator.up()` to `migrator.down()` and save the file. The latest migration will be reverted automatically when the server reloads.
1. **Important:** After the migration is reverted, change `migrator.down()` back to `migrator.up()` to ensure future migrations run correctly.
1. For more instructions, see the [umzug documentation](https://github.com/sequelize/umzug?tab=readme-ov-file#usage).
+1 -1
View File
@@ -32,4 +32,4 @@ RUN npm ci --omit=dev
EXPOSE 8082
CMD ["node", "dist/index.js"]
CMD ["node", "dist/src/index.js"]
+1 -1
View File
@@ -3,7 +3,7 @@
"private": true,
"scripts": {
"build": "tsc",
"start": "node ./dist/index.js",
"start": "node ./dist/src/index.js",
"dev": "nodemon --exec \"node --import tsx ./src/index.ts\" --ext ts,js,json --watch src"
},
"dependencies": {
+1 -1
View File
@@ -7,7 +7,7 @@
"module": "nodenext",
"moduleResolution": "nodenext",
"rootDir": "src",
"rootDir": ".",
"outDir": "dist",
"esModuleInterop": true,
+17 -4
View File
@@ -1,3 +1,16 @@
FROM registry.access.redhat.com/ubi10/nodejs-24-minimal AS build
WORKDIR /opt/app-root/src
ENV TZ="Europe/Helsinki"
COPY ./.npmrc .
COPY ./package*.json ./
RUN npm ci --include=dev
COPY . .
RUN npm run build
FROM registry.access.redhat.com/ubi10/nodejs-24-minimal
WORKDIR /opt/app-root/src
@@ -12,9 +25,9 @@ ENV SENTRY_ENVIRONMENT=$SENTRY_ENVIRONMENT
ARG SENTRY_RELEASE
ENV SENTRY_RELEASE=$SENTRY_RELEASE
COPY ./.npmrc .
COPY ./package*.json ./
COPY --from=build /opt/app-root/src/.npmrc .
COPY --from=build /opt/app-root/src/package*.json ./
COPY --from=build /opt/app-root/src/dist ./dist
RUN npm ci --omit=dev
COPY . .
CMD ["node", "src/index.js"]
CMD ["node", "dist/src/index.js"]
+1 -1
View File
@@ -6,7 +6,7 @@ The **sis-updater-worker** is responsible for processing BullMQ jobs that have b
When a job is picked up, the worker fetches the relevant entities from the **importer** database based on the `entityIds` specified in the job data. It then processes the data as needed and saves it into the **Oodikone database**.
The different types of jobs are defined in [`src/processor.js`](./src/processor.js). Below is a brief overview of the job types:
The different types of jobs are defined in [`src/processor.ts`](./src/processor.ts). Below is a brief overview of the job types:
- **prepurge_start**:
+1 -1
View File
@@ -9,4 +9,4 @@ ENV NODE_ENV=development
COPY . .
RUN npm ci --include=dev
CMD ["node_modules/.bin/nodemon", "--exec", "node src/index.js", "--watch", "src", "--watch", "--ext", "ts, js, json", "--legacy-watch"]
CMD ["node_modules/.bin/nodemon", "--exec", "node --import tsx src/index.ts", "--watch", "src", "--ext", "ts, js, json", "--legacy-watch"]
+907 -8
View File
@@ -20,7 +20,12 @@
"winston-transport-sentry-node": "^3.0.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.1",
"@types/pg": "^8.18.0",
"nodemon": "3.1.14",
"tsx": "^4.19.4",
"typescript": "^7.0.2",
"vitest": "4.1.9"
}
},
@@ -75,6 +80,448 @@
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@ioredis/commands": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz",
@@ -1811,6 +2258,23 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.25",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz",
"integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
@@ -1828,12 +2292,12 @@
}
},
"node_modules/@types/node": {
"version": "24.3.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
"integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
"version": "24.13.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.5.tgz",
"integrity": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.10.0"
"undici-types": "~7.18.0"
}
},
"node_modules/@types/pg": {
@@ -1841,7 +2305,6 @@
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz",
"integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
@@ -1887,6 +2350,346 @@
"integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==",
"license": "MIT"
},
"node_modules/@typescript/typescript-aix-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-loong64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-mips64el": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-riscv64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-s390x": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-sunos-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@vitest/expect": {
"version": "4.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz",
@@ -2345,6 +3148,48 @@
"dev": true,
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.2",
"@esbuild/android-arm": "0.28.2",
"@esbuild/android-arm64": "0.28.2",
"@esbuild/android-x64": "0.28.2",
"@esbuild/darwin-arm64": "0.28.2",
"@esbuild/darwin-x64": "0.28.2",
"@esbuild/freebsd-arm64": "0.28.2",
"@esbuild/freebsd-x64": "0.28.2",
"@esbuild/linux-arm": "0.28.2",
"@esbuild/linux-arm64": "0.28.2",
"@esbuild/linux-ia32": "0.28.2",
"@esbuild/linux-loong64": "0.28.2",
"@esbuild/linux-mips64el": "0.28.2",
"@esbuild/linux-ppc64": "0.28.2",
"@esbuild/linux-riscv64": "0.28.2",
"@esbuild/linux-s390x": "0.28.2",
"@esbuild/linux-x64": "0.28.2",
"@esbuild/netbsd-arm64": "0.28.2",
"@esbuild/netbsd-x64": "0.28.2",
"@esbuild/openbsd-arm64": "0.28.2",
"@esbuild/openbsd-x64": "0.28.2",
"@esbuild/openharmony-arm64": "0.28.2",
"@esbuild/sunos-x64": "0.28.2",
"@esbuild/win32-arm64": "0.28.2",
"@esbuild/win32-ia32": "0.28.2",
"@esbuild/win32-x64": "0.28.2"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -4174,6 +5019,25 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.23.13",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz",
"integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.28.0"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
@@ -4186,6 +5050,41 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc"
},
"engines": {
"node": ">=16.20.0"
},
"optionalDependencies": {
"@typescript/typescript-aix-ppc64": "7.0.2",
"@typescript/typescript-darwin-arm64": "7.0.2",
"@typescript/typescript-darwin-x64": "7.0.2",
"@typescript/typescript-freebsd-arm64": "7.0.2",
"@typescript/typescript-freebsd-x64": "7.0.2",
"@typescript/typescript-linux-arm": "7.0.2",
"@typescript/typescript-linux-arm64": "7.0.2",
"@typescript/typescript-linux-loong64": "7.0.2",
"@typescript/typescript-linux-mips64el": "7.0.2",
"@typescript/typescript-linux-ppc64": "7.0.2",
"@typescript/typescript-linux-riscv64": "7.0.2",
"@typescript/typescript-linux-s390x": "7.0.2",
"@typescript/typescript-linux-x64": "7.0.2",
"@typescript/typescript-netbsd-arm64": "7.0.2",
"@typescript/typescript-netbsd-x64": "7.0.2",
"@typescript/typescript-openbsd-arm64": "7.0.2",
"@typescript/typescript-openbsd-x64": "7.0.2",
"@typescript/typescript-sunos-x64": "7.0.2",
"@typescript/typescript-win32-arm64": "7.0.2",
"@typescript/typescript-win32-x64": "7.0.2"
}
},
"node_modules/umzug": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/umzug/-/umzug-3.8.3.tgz",
@@ -4210,9 +5109,9 @@
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.10.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"license": "MIT"
},
"node_modules/universalify": {
+9 -3
View File
@@ -1,10 +1,11 @@
{
"name": "sis-updater-worker",
"private": true,
"type": "module",
"scripts": {
"start": "node ./src/index.js",
"dev": "nodemon ./src/index.js",
"build": "tsc && npm run copy:csv",
"copy:csv": "cp src/updater/updateStudents/excludedPartialAttainments.csv dist/src/updater/updateStudents/",
"start": "node ./dist/src/index.js",
"dev": "nodemon --exec \"node --import tsx ./src/index.ts\" --ext ts,js,json --watch src",
"test": "vitest run",
"test:unit": "vitest run ./tests/unit",
"test:watch": "vitest"
@@ -24,7 +25,12 @@
"winston-transport-sentry-node": "^3.0.0"
},
"devDependencies": {
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.1",
"@types/pg": "^8.18.0",
"nodemon": "3.1.14",
"tsx": "^4.19.4",
"typescript": "^7.0.2",
"vitest": "4.1.9"
}
}
@@ -1,17 +1,17 @@
export const isDev = process.env.NODE_ENV === 'development'
export const isStaging = process.env.STAGING || false
export const isStaging = process.env.STAGING === 'true'
export const isProduction = process.env.NODE_ENV === 'production'
export const MIGRATIONS_LOCK = 'MIGRATIONS_LOCK'
export const PURGE_LOCK = 'PURGE_LOCK'
export const REDIS_HOST = process.env.REDIS_HOST || 'redis-updater'
export const REDIS_PORT = process.env.REDIS_PORT || 6379
export const rootOrgId = process.env.ROOT_ORG_ID || 'hy-university-root-id'
export const REDIS_HOST = process.env.REDIS_HOST ?? 'redis-updater'
export const REDIS_PORT = Number(process.env.REDIS_PORT) || 6379
export const rootOrgId = process.env.ROOT_ORG_ID ?? 'hy-university-root-id'
export const runningInCI = process.env.CI === 'true'
export const { SENTRY_DSN } = process.env
export const { DB_URL } = process.env
export const { SIS_IMPORTER_HOST } = process.env
export const { SIS_IMPORTER_PORT } = process.env
export const SIS_IMPORTER_PORT = Number(process.env.SIS_IMPORTER_PORT) || undefined
export const { SIS_IMPORTER_USER } = process.env
export const { SIS_IMPORTER_PASSWORD } = process.env
export const { SIS_IMPORTER_DATABASE } = process.env
@@ -1,5 +1,5 @@
import { EventEmitter } from 'events'
import knex from 'knex'
import knex, { type Knex } from 'knex'
import { Sequelize } from 'sequelize'
import { Umzug, SequelizeStorage } from 'umzug'
@@ -14,22 +14,22 @@ import {
SIS_IMPORTER_PASSWORD,
SIS_IMPORTER_DATABASE,
SIS_PASSWORD,
} from '../config.js'
import logger from '../utils/logger.js'
import { lock } from '../utils/redis.js'
} from '../config'
import logger from '../utils/logger'
import { lock } from '../utils/redis'
class DbConnections extends EventEmitter {
RETRY_ATTEMPTS = 15
knexConnection = false
seqConnection = false
/** @type {import('knex').Knex | undefined}
knex = undefined
/** @type {import('knex').Knex | undefined} */
knex!: Knex
/** @type {import('sequelize').Sequelize | undefined} */
sequelize = undefined
sequelize: Sequelize
constructor() {
super()
this.sequelize = new Sequelize(DB_URL, {
this.sequelize = new Sequelize(DB_URL!, {
dialect: 'postgres',
pool: {
max: 25,
@@ -46,7 +46,7 @@ class DbConnections extends EventEmitter {
* Registers named connection as established and emits 'connect' to the scheduler.
* @param {'knexConnection' | 'seqConnection'} conn
*/
establish(conn) {
establish(conn: 'knexConnection' | 'seqConnection') {
this[conn] = true
if (this.knexConnection && this.seqConnection) this.emit('connect')
}
@@ -88,7 +88,7 @@ class DbConnections extends EventEmitter {
return
}
logger.error(`Knex database connection failed! Attempt ${attempt}/${this.RETRY_ATTEMPTS}`)
setTimeout(() => this.connect(attempt + 1), 1000 * attempt)
setTimeout(() => void this.connect(attempt + 1), 1000 * attempt)
}
}
@@ -104,9 +104,9 @@ class DbConnections extends EventEmitter {
tableName: 'migrations',
}),
migrations: {
glob: 'src/db/migrations/*.cjs',
glob: ['migrations/*.cjs', { cwd: __dirname }],
resolve: ({ name, path, context }) => {
const getMigration = () => import(path)
const getMigration = async () => await import(path!)
return {
// Migration names need to end in .js for legacy reasons
// modifying this behaviour requires manually altering the db 'migrations' table
@@ -1,6 +1,6 @@
import { getLatestSnapshot, isActive, getActiveSnapshot, getLatestActiveSnapshot } from '../utils/index.js'
import logger from '../utils/logger.js'
import { dbConnections } from './connection.js'
import { getLatestSnapshot, isActive, getActiveSnapshot, getLatestActiveSnapshot } from '../utils/index'
import logger from '../utils/logger'
import { dbConnections } from './connection'
/**
* @param {string} table - Table this function is trying to query against
@@ -9,7 +9,8 @@ import { dbConnections } from './connection.js'
*
* @returns {Promise<T>} Where T is the query result object
*/
export const selectOneById = async (table, id, col = 'id') => dbConnections.knex(table).where(col, id).first()
export const selectOneById = async (table: string, id: string, col = 'id') =>
dbConnections.knex(table).where(col, id).first()
/**
* @param {string} table - Table this function is trying to query against
@@ -18,7 +19,8 @@ export const selectOneById = async (table, id, col = 'id') => dbConnections.knex
*
* @returns {Promise<T>} Where T is the query result object
*/
export const selectLastById = async (table, id, col = 'id') => (await dbConnections.knex(table).where(col, id)).at(-1)
export const selectLastById = async (table: string, id: string, col = 'id') =>
(await dbConnections.knex(table).where(col, id)).at(-1)
/**
* @param {string} table - Table this function is trying to query against
@@ -27,7 +29,8 @@ export const selectLastById = async (table, id, col = 'id') => (await dbConnecti
*
* @returns {Promise<T[]>} Where T is the query result object
*/
export const selectFromByIds = async (table, ids, col = 'id') => dbConnections.knex(table).whereIn(col, ids)
export const selectFromByIds = async (table: string, ids: string[], col = 'id') =>
dbConnections.knex(table).whereIn(col, ids)
/**
* @param {string} table - Table this function is trying to query against
@@ -38,22 +41,27 @@ export const selectFromByIds = async (table, ids, col = 'id') => dbConnections.k
*
* @returns {Promise<T[]>} Where T is the query result object
*/
export const selectFromByIdsOrderBy = async (table, ids, col = 'id', by = 'id', order = 'asc') =>
dbConnections.knex(table).whereIn(col, ids).orderBy(by, order)
export const selectFromByIdsOrderBy = async (
table: string,
ids: string[],
col = 'id',
by = 'id',
order: 'asc' | 'desc' = 'asc'
) => dbConnections.knex(table).whereIn(col, ids).orderBy(by, order)
/**
* @param {string} table - Table this function is trying to query against
*
* @returns {Promise<T[]>} Where T is the query result object
*/
export const selectAllFrom = async table => dbConnections.knex(table)
export const selectAllFrom = async (table: string) => dbConnections.knex(table)
/**
* @param {string} table - Table this function is trying to query against
*
* @returns {Promise<T[]>} Where T is the query result object
*/
export const selectAllFromSnapshots = async table =>
export const selectAllFromSnapshots = async (table: string) =>
(
await dbConnections.knex
.select(dbConnections.knex.raw(`array_agg(to_json(${table}.*)) as data`))
@@ -71,7 +79,7 @@ export const selectAllFromSnapshots = async table =>
*
* @returns {Promise<T[]>} Where T is the query result object
*/
export const selectFromSnapshotsByIds = async (table, ids, col = 'id') =>
export const selectFromSnapshotsByIds = async (table: string, ids: string[], col = 'id') =>
(
await dbConnections.knex
.select(dbConnections.knex.raw(`array_agg(to_json(${table}.*)) as data`))
@@ -90,7 +98,7 @@ export const selectFromSnapshotsByIds = async (table, ids, col = 'id') =>
*
* @returns {Promise<T[]>} Where T is the query result object
*/
export const selectLatestActiveFromSnapshotsByIds = async (table, ids, col = 'id') =>
export const selectLatestActiveFromSnapshotsByIds = async (table: string, ids: string[], col = 'id') =>
(
await dbConnections.knex
.select(dbConnections.knex.raw(`array_agg(to_json(${table}.*)) as data`))
@@ -108,7 +116,7 @@ export const selectLatestActiveFromSnapshotsByIds = async (table, ids, col = 'id
*
* @returns {Promise<T[]>} Where T is the query result object
*/
export const selectFromActiveSnapshotsByIds = async (table, ids, col = 'id') =>
export const selectFromActiveSnapshotsByIds = async (table: string, ids: string[], col = 'id') =>
(
await dbConnections.knex
.select(dbConnections.knex.raw(`array_agg(to_json(${table}.*)) as data`))
@@ -119,7 +127,8 @@ export const selectFromActiveSnapshotsByIds = async (table, ids, col = 'id') =>
.map(({ data }) => getActiveSnapshot(data))
.filter(s => !!s)
const getColumnsToUpdate = (model, keys) => Object.keys(model.rawAttributes).filter(a => !keys.includes(a))
// TODO: Type this
const getColumnsToUpdate = (model, keys: string[]) => Object.keys(model.rawAttributes).filter(a => !keys.includes(a))
/**
* @param {import('sequelize').Model<T>} model - Model for the bulkCreate
@@ -133,7 +142,7 @@ export const bulkCreate = async (
model,
entities,
transaction = null,
properties = ['id'],
properties: string[] = ['id'],
upsertStyle = true,
findBy = 'id'
) => {
@@ -144,7 +153,7 @@ export const bulkCreate = async (
: { ignoreDuplicates: true, transaction }
await model.bulkCreate(entities, options)
} catch (_error) {
const failures = []
const failures: any[] = []
for (const entity of entities) {
try {
if (upsertStyle) await model.upsert(entity, { fields: getColumnsToUpdate(model, properties) })
@@ -156,7 +165,7 @@ export const bulkCreate = async (
defaults: entity,
})
}
} catch (error) {
} catch (error: any) {
// Skip errors with credits pointing to non-existent course_ids. Credit just doesn't get created
if (
error.name === 'SequelizeForeignKeyConstraintError' &&
@@ -1,7 +1,9 @@
import { Model, ARRAY, BOOLEAN, DATE, INTEGER, JSONB, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class SISStudyRight extends Model {}
class SISStudyRight extends Model {
[key: string]: any
}
SISStudyRight.init(
{
@@ -1,8 +1,10 @@
import { Model, BOOLEAN, DATE, INTEGER, JSONB, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class SISStudyRightElement extends Model {}
class SISStudyRightElement extends Model {
[key: string]: any
}
SISStudyRightElement.init(
{
@@ -1,8 +1,10 @@
import { Model, BOOLEAN, DATE, JSONB, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class Course extends Model {}
class Course extends Model {
[key: string]: any
}
Course.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, JSONB, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class CourseProvider extends Model {}
class CourseProvider extends Model {
[key: string]: any
}
CourseProvider.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, JSONB, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class CourseType extends Model {}
class CourseType extends Model {
[key: string]: any
}
CourseType.init(
{
@@ -1,8 +1,18 @@
import { Model, BOOLEAN, DATE, DOUBLE, INTEGER, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class Credit extends Model {}
class Credit extends Model {
[key: string]: any
static notUnnecessary: (credit: Credit) => boolean
static passed: (credit: Credit) => boolean
static failed: (credit: Credit) => boolean
static improved: (credit: Credit) => boolean
}
Credit.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class CreditTeacher extends Model {}
class CreditTeacher extends Model {
[key: string]: any
}
CreditTeacher.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, INTEGER, JSONB } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class CreditType extends Model {}
class CreditType extends Model {
[key: string]: any
}
CreditType.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, JSONB, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class CurriculumPeriod extends Model {}
class CurriculumPeriod extends Model {
[key: string]: any
}
CurriculumPeriod.init(
{
@@ -1,8 +1,10 @@
import { Model, BOOLEAN, DATE, INTEGER, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class Enrollment extends Model {}
class Enrollment extends Model {
[key: string]: any
}
Enrollment.init(
{
@@ -1,21 +1,21 @@
import Course from './course.js'
import CourseProvider from './courseProvider.js'
import CourseType from './courseType.js'
import Credit from './credit.js'
import CreditTeacher from './creditTeacher.js'
import CreditType from './creditType.js'
import CurriculumPeriod from './curriculumPeriod.js'
import Enrollment from './enrollment.js'
import Organization from './organization.js'
import ProgrammeModule from './programmeModule.js'
import ProgrammeModuleChild from './programmeModuleChild.js'
import Semester from './semester.js'
import SISStudyRight from './SISStudyRight.js'
import SISStudyRightElement from './SISStudyRightElement.js'
import Student from './student.js'
import Studyplan from './studyplan.js'
import StudyrightExtent from './studyrightExtent.js'
import Teacher from './teacher.js'
import Course from './course'
import CourseProvider from './courseProvider'
import CourseType from './courseType'
import Credit from './credit'
import CreditTeacher from './creditTeacher'
import CreditType from './creditType'
import CurriculumPeriod from './curriculumPeriod'
import Enrollment from './enrollment'
import Organization from './organization'
import ProgrammeModule from './programmeModule'
import ProgrammeModuleChild from './programmeModuleChild'
import Semester from './semester'
import SISStudyRight from './SISStudyRight'
import SISStudyRightElement from './SISStudyRightElement'
import Student from './student'
import Studyplan from './studyplan'
import StudyrightExtent from './studyrightExtent'
import Teacher from './teacher'
const CREDIT_TYPE_CODES = {
PASSED: 4,
@@ -1,8 +1,10 @@
import { Model, DATE, JSONB, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class Organization extends Model {}
class Organization extends Model {
[key: string]: any
}
Organization.init(
{
@@ -13,6 +15,8 @@ Organization.init(
code: {
type: STRING,
references: {
// @ts-expect-error FIXME: These fields acually don't exist. They most likely should be "model" and "key"
// but that doesn't make any sense: Foreign key to itself with "code" as key.
table: 'organization',
field: 'code',
},
@@ -1,8 +1,10 @@
import { Model, ARRAY, DATE, INTEGER, JSONB, STRING, TEXT } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class ProgrammeModule extends Model {}
class ProgrammeModule extends Model {
[key: string]: any
}
ProgrammeModule.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class ProgrammeModuleChild extends Model {}
class ProgrammeModuleChild extends Model {
[key: string]: any
}
ProgrammeModuleChild.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, INTEGER, JSONB, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class Semester extends Model {}
class Semester extends Model {
[key: string]: any
}
Semester.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, INTEGER, STRING, BOOLEAN, JSONB } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class Student extends Model {}
class Student extends Model {
[key: string]: any
}
Student.init(
{
@@ -1,8 +1,10 @@
import { Model, ARRAY, DATE, DOUBLE, STRING, TEXT } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class Studyplan extends Model {}
class Studyplan extends Model {
[key: string]: any
}
Studyplan.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, INTEGER, JSONB } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class StudyrightExtent extends Model {}
class StudyrightExtent extends Model {
[key: string]: any
}
StudyrightExtent.init(
{
@@ -1,8 +1,10 @@
import { Model, DATE, STRING } from 'sequelize'
import { dbConnections } from '../connection.js'
import { dbConnections } from '../connection'
class Teacher extends Model {}
class Teacher extends Model {
[key: string]: any
}
Teacher.init(
{
@@ -1,6 +1,6 @@
import { dbConnections } from './db/connection.js'
import logger from './utils/logger.js'
import './worker.js'
import { dbConnections } from './db/connection'
import logger from './utils/logger'
import './worker'
dbConnections
.connect()
@@ -1,12 +1,9 @@
import { intersection } from 'lodash-es'
import { update } from './updater/index.js'
import { purgeByStudentNumber, prePurge, purge } from './updater/purge.js'
import { loadMapsOnDemand, nukeRedis } from './updater/shared.js'
import {
fixVarhaiskasvatusStudyRights,
studentsThatNeedToBeFixed,
} from './updater/updateStudents/varhaiskasvatusFixer.js'
import { update } from './updater/index'
import { purgeByStudentNumber, prePurge, purge } from './updater/purge'
import { loadMapsOnDemand, nukeRedis } from './updater/shared'
import { fixVarhaiskasvatusStudyRights, studentsThatNeedToBeFixed } from './updater/updateStudents/varhaiskasvatusFixer'
const updateMsgHandler = async updateMsg => {
await update(updateMsg)
@@ -1,5 +1,5 @@
import { selectFromByIds, selectLatestActiveFromSnapshotsByIds } from '../db/index.js'
import { creditTypeIdsToCreditTypes } from './shared.js'
import { selectFromByIds, selectLatestActiveFromSnapshotsByIds } from '../db/index'
import { creditTypeIdsToCreditTypes } from './shared'
import {
updateOrganisations,
updateStudyModules,
@@ -8,9 +8,9 @@ import {
updateCreditTypes,
updateStudyrightExtents,
updateCurriculumPeriods,
} from './updateMeta.js'
import { updateProgrammeModules } from './updateProgrammeModules/updateProgrammeModules.js'
import { updateStudents } from './updateStudents/index.js'
} from './updateMeta'
import { updateProgrammeModules } from './updateProgrammeModules/updateProgrammeModules'
import { updateStudents } from './updateStudents/index'
const idToHandler = {
students: updateStudents,
@@ -1,6 +1,6 @@
import { sortBy, flatten, uniqBy } from 'lodash-es'
import logger from '../utils/logger.js'
import logger from '../utils/logger'
import {
educationTypeToExtentcode,
getCountry,
@@ -8,7 +8,7 @@ import {
getGrade,
getSemesterByDate,
getUniOrgId,
} from './shared.js'
} from './shared'
// Keeping previous oodi logic:
// 0 = Not known 'urn:code:gender:not-known'
@@ -57,7 +57,7 @@ const calculateTotalCreditsFromAttainments = attainments => {
if (!attainment.primary) return false
if (attainment.expiryDate < now) return false
// Does not have any attainments attached to it, so is not a study module whose attainments have already been counted
if (attainment.nodes && attainment.nodes[0] !== undefined) return false
if (attainment.nodes?.[0] !== undefined) return false
if (!validAttainmentTypes.includes(attainment.type)) return false
// If the state is FAILED or IMPROVED it should not be counted to total
if (!validStates.includes(attainment.state)) return false
@@ -349,7 +349,7 @@ export const studyplanMapper =
const code = programmeModuleIdToCode[programmeId]
if (!code) return null
const graduated = moduleAttainments[programmeId] && moduleAttainments[programmeId][studyplan.user_id]
const graduated = moduleAttainments[programmeId]?.[studyplan.user_id]
const id = `${studentnumber}-${code}-${studyrightId}`
// NB: values are course_unit_ids, matching Credit/Enrollment's course_id
@@ -371,9 +371,7 @@ export const studyplanMapper =
studyplan.module_selections
.filter(
({ moduleId }) =>
moduleIdToParentModuleCode[moduleId]?.has(code) &&
moduleAttainments[moduleId] &&
moduleAttainments[moduleId][studyplan.user_id]
moduleIdToParentModuleCode[moduleId]?.has(code) && moduleAttainments[moduleId]?.[studyplan.user_id]
)
.map(({ moduleId }) => getCourseIdsFromAttainment(moduleAttainments[moduleId][studyplan.user_id]))
)
@@ -428,8 +426,7 @@ export const studyplanMapper =
.filter(
({ moduleId }) =>
moduleIdToParentModuleCode[moduleId]?.has(code) &&
moduleAttainments[moduleId] &&
moduleAttainments[moduleId][studyplan.user_id]
moduleAttainments[moduleId]?.[studyplan.user_id]
)
.map(({ moduleId }) => getAttainmentsFromAttainment(moduleAttainments[moduleId][studyplan.user_id]))
)
@@ -1,6 +1,6 @@
import { Op } from 'sequelize'
import { PURGE_LOCK } from '../config.js'
import { PURGE_LOCK } from '../config'
import {
Organization,
Course,
@@ -14,9 +14,9 @@ import {
StudyrightExtent,
SISStudyRightElement,
SISStudyRight,
} from '../db/models/index.js'
import logger from '../utils/logger.js'
import { lock } from '../utils/redis.js'
} from '../db/models/index'
import logger from '../utils/logger'
import { lock } from '../utils/redis'
const tableToModel = {
course: Course,
@@ -1,18 +1,18 @@
import { each } from 'async'
import { selectAllFrom, selectAllFromSnapshots } from '../db/index.js'
import { Semester, Organization, CREDIT_TYPE_CODES } from '../db/models/index.js'
import { redisClient, lock as redisLock } from '../utils/redis.js'
import { selectAllFrom, selectAllFromSnapshots } from '../db/index'
import { Semester, Organization, CREDIT_TYPE_CODES } from '../db/models/index'
import { redisClient, lock as redisLock } from '../utils/redis'
const getRedisJSON = async key => JSON.parse(await redisClient.get(key))
const setRedisJSON = async (key, value) => redisClient.set(key, JSON.stringify(value))
const getRedisJSON = async (key: string) => JSON.parse((await redisClient.get(key))!)
const setRedisJSON = async (key: string, value) => redisClient.set(key, JSON.stringify(value))
const TIME_LIMIT_BETWEEN_RELOADS = 1000 * 60 * 30
const REDIS_INITIALIZED = 'INITIALIZED'
const SHARED_LOCK = 'SHARED_LOCK'
const FIRST_SEMESTER_START_YEAR = 1950
let loadedAt = null
let loadedAt: number | null = null
const localMapToRedisKey = {
daysToSemesters: 'DAYS_TO_SEMESTERS',
@@ -25,7 +25,7 @@ const localMapToRedisKey = {
countries: 'COUNTRIES',
}
const localMaps = {
const localMaps: Record<keyof typeof localMapToRedisKey, any> = {
daysToSemesters: null,
educationTypes: null,
organisationIdToCode: null,
@@ -37,7 +37,7 @@ const localMaps = {
}
const loadMapsFromRedis = async () =>
each(Object.entries(localMapToRedisKey), async ([localMap, redisKey]) => {
await each(Object.entries(localMapToRedisKey), async ([localMap, redisKey]: [any, string]) => {
localMaps[localMap] = await getRedisJSON(redisKey)
})
@@ -61,7 +61,7 @@ const initDaysToSemesters = async () => {
)
}
export const getSemesterByDate = date => localMaps.daysToSemesters[date.toDateString()]
export const getSemesterByDate = (date: Date) => localMaps.daysToSemesters[date.toDateString()]
const initEducationTypes = async () =>
setRedisJSON(
@@ -72,7 +72,7 @@ const initEducationTypes = async () =>
}, {})
)
export const getEducationType = id => localMaps.educationTypes[id]
export const getEducationType = (id: string) => localMaps.educationTypes[id]
const initOrganisationIdToCode = async () =>
setRedisJSON(
@@ -83,7 +83,7 @@ const initOrganisationIdToCode = async () =>
}, {})
)
export const getOrganisationCode = id => localMaps.organisationIdToCode[id]
export const getOrganisationCode = (id: string) => localMaps.organisationIdToCode[id]
const initEducationIdToEducation = async () =>
setRedisJSON(
@@ -94,7 +94,7 @@ const initEducationIdToEducation = async () =>
}, {})
)
export const getEducation = id => localMaps.educationIdToEducation[id]
export const getEducation = (id: string) => localMaps.educationIdToEducation[id]
const initGradeScaleIdToGradeIdsToGrades = async () =>
setRedisJSON(
@@ -114,7 +114,7 @@ const initGradeScaleIdToGradeIdsToGrades = async () =>
}, {})
)
export const getGrade = (scaleId, gradeId) => localMaps.gradeScaleIdToGradeIdsToGrades[scaleId][gradeId]
export const getGrade = (scaleId: string, gradeId: string) => localMaps.gradeScaleIdToGradeIdsToGrades[scaleId][gradeId]
const initOrgToUniOrgId = async () => {
const organisations = await selectAllFromSnapshots('organisations')
@@ -127,7 +127,7 @@ const initOrgToUniOrgId = async () => {
)
}
export const getUniOrgId = orgId => localMaps.orgToUniOrgId[orgId]
export const getUniOrgId = (orgId: string) => localMaps.orgToUniOrgId[orgId]
const initStartYearToSemesters = async () => {
const semesters = await Semester.findAll()
@@ -146,10 +146,10 @@ const initStartYearToSemesters = async () => {
]
}
setRedisJSON(localMapToRedisKey.startYearToSemesters, startingYearToSemesterObj)
await setRedisJSON(localMapToRedisKey.startYearToSemesters, startingYearToSemesterObj)
}
export const getSemesterCode = (studyYearStartYear, termIndex) => {
export const getSemesterCode = (studyYearStartYear: number, termIndex: number) => {
return localMaps.startYearToSemesters[studyYearStartYear][termIndex]
}
@@ -207,9 +207,9 @@ export const nukeRedis = async () => {
await unlock()
}
export const getCountry = countryId => localMaps.countries[countryId]
export const getCountry = (countryId: string) => localMaps.countries[countryId]
export const educationTypeToExtentcode = {
export const educationTypeToExtentcode: Record<string, number | null> = {
'urn:code:education-type:degree-education:bachelors-degree': 1,
'urn:code:education-type:degree-education:masters-degree': 2,
'urn:code:education-type:degree-education:lic': 3,
@@ -237,7 +237,7 @@ export const educationTypeToExtentcode = {
'urn:code:education-type:non-degree-education:separate-studies:adult-educator-pedagogical-studies': null, // Parent is 99
}
export const getCreditTypeCodeFromAttainment = (attainment, passed) => {
export const getCreditTypeCodeFromAttainment = (attainment, passed: boolean) => {
const { primary, state } = attainment
if (!passed || state === 'FAILED') return CREDIT_TYPE_CODES.FAILED
if (!primary) return CREDIT_TYPE_CODES.IMPROVED
@@ -264,4 +264,4 @@ const creditTypeIdToCreditType = {
},
}
export const creditTypeIdsToCreditTypes = ids => ids.map(id => creditTypeIdToCreditType[id])
export const creditTypeIdsToCreditTypes = (ids: number[]) => ids.map(id => creditTypeIdToCreditType[id])
@@ -1,6 +1,6 @@
import { uniqBy, groupBy } from 'lodash-es'
import { rootOrgId } from '../config.js'
import { bulkCreate, selectFromByIdsOrderBy } from '../db/index.js'
import { rootOrgId } from '../config'
import { bulkCreate, selectFromByIdsOrderBy } from '../db/index'
import {
Course,
CourseProvider,
@@ -9,26 +9,20 @@ import {
CurriculumPeriod,
Organization,
StudyrightExtent,
} from '../db/models/index.js'
import {
courseMapper,
courseProviderMapper,
mapCourseType,
mapStudyrightExtent,
mapCurriculumPeriod,
} from './mapper.js'
} from '../db/models/index'
import { courseMapper, courseProviderMapper, mapCourseType, mapStudyrightExtent, mapCurriculumPeriod } from './mapper'
export const updateOrganisations = async organisations => {
await bulkCreate(Organization, organisations)
}
const updateCourses = async (courseIdToAttainments, groupIdToCourse) => {
const courseProviders = []
const mappedCourses = []
const courseProviders: any[] = []
const mappedCourses: any[] = []
const mapCourse = courseMapper(courseIdToAttainments)
for (const [groupId, courses] of Object.entries(groupIdToCourse)) {
for (const [groupId, courses] of Object.entries<any>(groupIdToCourse)) {
const now = new Date()
const { id: primaryCourseId } = courses.reduce(
(acc, cur) => {
@@ -46,7 +40,7 @@ const updateCourses = async (courseIdToAttainments, groupIdToCourse) => {
/** @type string[][] @description nested arrays of courseUnit groupIds. Modules have no substitutions. */
const substitutionGroups = course.substitutions?.map(subGroup => subGroup.map(sub => sub.courseUnitGroupId)) ?? []
const organisationsById = {}
const organisationsById: Record<string, any> = {}
const { organisations, validity_period: courseValidityPeriod } = course
if (organisations) {
@@ -1,10 +1,17 @@
import type { Knex } from 'knex'
const anyModule = rule => ({ id: rule.localId, name: 'Any module' })
const anyCourse = rule => ({ id: rule.localId, name: 'Any course' })
const unknownRule = rule => ({ type: rule.type, fact: 'Unhandled rule' })
const unknownModule = mod => ({ name: mod.name, type: 'unknown' })
export class ModuleResolver {
constructor(knex) {
knex: Knex
moduleCache
orderTable: Record<string, number>
order: number
constructor(knex: Knex) {
/** @type {import('knex').Knex} */
this.knex = knex
this.moduleCache = {}
@@ -12,7 +19,7 @@ export class ModuleResolver {
this.order = 1
}
getOrder(group_id) {
getOrder(group_id: string) {
const oldOrder = this.orderTable[group_id]
if (oldOrder) return oldOrder
const newOrder = this.order++
@@ -71,7 +78,7 @@ export class ModuleResolver {
const id = rule.moduleGroupId
const modules = await this.knex('modules').where({ group_id: id })
const children = []
const children: any[] = []
for (const mod of modules) {
if (mod.code?.startsWith('KK-')) continue
@@ -86,7 +93,7 @@ export class ModuleResolver {
}
async compositeResolver(rule) {
const children = []
const children: any[] = []
for (const r of rule.rules) {
const result = await this.resolve(r)
children.push(result)
@@ -104,7 +111,7 @@ export class ModuleResolver {
return courses.map(course => this.createCourse(course))
}
async resolve(rule) {
async resolve(rule): Promise<any> {
if (rule.type === 'CreditsRule') {
return await this.resolve(rule.rule)
}
@@ -1,10 +1,10 @@
import { chunk } from 'lodash-es'
import { dbConnections } from '../../db/connection.js'
import { bulkCreate, selectFromByIds } from '../../db/index.js'
import { ProgrammeModule, ProgrammeModuleChild } from '../../db/models/index.js'
import logger from '../../utils/logger.js'
import { ModuleResolver } from './resolver.js'
import { dbConnections } from '../../db/connection'
import { bulkCreate, selectFromByIds } from '../../db/index'
import { ProgrammeModule, ProgrammeModuleChild } from '../../db/models/index'
import logger from '../../utils/logger'
import { ModuleResolver } from './resolver'
const resolveProgramme = async programme => {
const responsibleOrg = programme.organisations
@@ -1,14 +1,14 @@
import { selectAllFrom, bulkCreate, selectOneById, selectLastById } from '../../db/index.js'
import { SISStudyRight, SISStudyRightElement } from '../../db/models/index.js'
import logger from '../../utils/logger.js'
import { termRegistrationTypeToEnrollmenttype } from '../mapper.js'
import { selectAllFrom, bulkCreate, selectOneById, selectLastById } from '../../db/index'
import { SISStudyRight, SISStudyRightElement } from '../../db/models/index'
import logger from '../../utils/logger'
import { termRegistrationTypeToEnrollmenttype } from '../mapper'
import {
getEducation,
getSemesterCode,
getEducationType,
educationTypeToExtentcode,
getOrganisationCode,
} from '../shared.js'
} from '../shared'
const TVEX_URN_CODE = 'urn:code:custom:hy-university-root-id:katu:katu1'
@@ -18,7 +18,7 @@ const normalizeDateTime = date =>
date.toLocaleString('se-SE', { timeZone: 'Europe/Helsinki', year: 'numeric', month: '2-digit', day: '2-digit' })
)
const addDaysToDate = (date, days) => {
const addDaysToDate = (date, days: number) => {
if (!date) return null
const newDate = new Date(date)
newDate.setDate(newDate.getDate() + days)
@@ -74,7 +74,12 @@ const studyRightMapper = (personIdToStudentNumber, admissionNamesById, semesterE
}
}
const getCorrectStartDateForProgramme = (phase, isFirstProgramme, latestSnapshot, startDateFromSnapshot) => {
const getCorrectStartDateForProgramme = (
phase: number,
isFirstProgramme: boolean,
latestSnapshot,
startDateFromSnapshot
) => {
if (phase === 1) {
return isFirstProgramme ? new Date(latestSnapshot.valid.startDate) : startDateFromSnapshot
}
@@ -87,8 +92,8 @@ const getCorrectStartDateForProgramme = (phase, isFirstProgramme, latestSnapshot
// Each programme (educationPhase1/2GroupId) has their own study right element.
// Study tracks (educationPhase1/2ChildGroupId) belong to the corresponding study right element.
const studyRightElementMapper =
(phase, latestSnapshot) =>
async ([groupId, { startDate, endDate, code, studyTrack: elementStudyTrack, graduated }]) => {
(phase: number, latestSnapshot) =>
async ([groupId, { startDate, endDate, code, studyTrack: elementStudyTrack, graduated }]: [string, any]) => {
const educationInfo = await selectOneById('modules', groupId.replace('EDU', 'DP'), 'group_id')
let studyTrack = elementStudyTrack ?? null
@@ -119,12 +124,12 @@ export const updateSISStudyRights = async (groupedStudyRights, personIdToStudent
const mapStudyRight = studyRightMapper(personIdToStudentNumber, admissionTypes, semesterEnrollments)
// Latest snapshot of each study right
const latestStudyRights = Object.values(groupedStudyRights).reduce((acc, curr) => {
const latestStudyRights = Object.values<any>(groupedStudyRights).reduce((acc: any[], curr) => {
acc.push(curr[0])
return acc
}, [])
const formattedStudyRights = latestStudyRights.map(mapStudyRight).filter(Boolean)
const formattedStudyRights = latestStudyRights.map(mapStudyRight).filter(studyRight => !!studyRight)
await bulkCreate(SISStudyRight, formattedStudyRights)
return new Set(formattedStudyRights.map(studyRight => studyRight.id))
@@ -153,9 +158,9 @@ const buildStudyRightElements = (studyRightSnapshots, groupIdToCode) => {
const phase1Elements = {}
const phase2Elements = {}
let latestValidSnapshot = null
let lastPhase1Id = null
let lastPhase2Id = null
let latestValidSnapshot: any = null
let lastPhase1Id: string | null = null
let lastPhase2Id: string | null = null
let graduatedPhase1 = false
let graduatedPhase2 = false
@@ -180,7 +185,7 @@ const buildStudyRightElements = (studyRightSnapshots, groupIdToCode) => {
// In that case fallback to the start of the study rights validity period.
const normalizedDateTime = normalizeDateTime(snapshot_date_time || valid.startDate)
const addElementToMap = (phase, phaseElements, groupId) => {
const addElementToMap = (phase: number, phaseElements, groupId: string) => {
// Skip elements that don't have id matching to a programme
if (!(groupId in groupIdToCode)) return
@@ -208,24 +213,24 @@ const buildStudyRightElements = (studyRightSnapshots, groupIdToCode) => {
// (Oodikone can't display the same study right element twice with different start/enddates anyway).
if (!isNewGroupId) {
delete phaseElements[lastPhaseId]
delete phaseElements[groupId]['endDate']
delete phaseElements[groupId].endDate
} else {
phaseElements[lastPhaseId]['endDate'] = addDaysToDate(normalizedDateTime, -1)
phaseElements[lastPhaseId].endDate = addDaysToDate(normalizedDateTime, -1)
}
}
if (phase === 1) {
lastPhase1Id = groupId
if (study_right_graduation?.phase1GraduationDate) {
phaseElements[groupId]['endDate'] = normalizeDateTime(study_right_graduation.phase1GraduationDate)
phaseElements[groupId]['graduated'] = true
phaseElements[groupId].endDate = normalizeDateTime(study_right_graduation.phase1GraduationDate)
phaseElements[groupId].graduated = true
graduatedPhase1 = true
}
} else {
lastPhase2Id = groupId
if (study_right_graduation?.phase2GraduationDate) {
phaseElements[groupId]['endDate'] = normalizeDateTime(study_right_graduation.phase2GraduationDate)
phaseElements[groupId]['graduated'] = true
phaseElements[groupId].endDate = normalizeDateTime(study_right_graduation.phase2GraduationDate)
phaseElements[groupId].graduated = true
graduatedPhase2 = true
}
}
@@ -262,15 +267,15 @@ const buildStudyRightElements = (studyRightSnapshots, groupIdToCode) => {
// We only take one study track per groupId (degree programme).
// Study track from a newer snapshot will replace the previous one.
// If there's no study track in a newer snapshot with the same groupId, we conclude that the study track has been removed.
const addStudyTracks = (groupId, childGroupId, phaseElements) => {
const addStudyTracks = (groupId: string, childGroupId: string, phaseElements) => {
if (!phaseElements[groupId]) return
if (childGroupId) {
phaseElements[groupId]['studyTrack'] = {
phaseElements[groupId].studyTrack = {
id: childGroupId,
code: groupIdToCode[childGroupId],
}
} else if (!childGroupId && 'studyTrack' in phaseElements[groupId]) {
delete phaseElements[groupId]['studyTrack']
delete phaseElements[groupId].studyTrack
}
}
@@ -283,14 +288,14 @@ const buildStudyRightElements = (studyRightSnapshots, groupIdToCode) => {
// (it overshoots by a day so subtract that)
const addMissingEndDates = phaseElements => {
for (const groupId of Object.keys(phaseElements)) {
phaseElements[groupId]['endDate'] ??= addDaysToDate(latestValidSnapshot.valid.endDate, -1)
phaseElements[groupId].endDate ??= addDaysToDate(latestValidSnapshot.valid.endDate, -1)
}
}
addMissingEndDates(phase1Elements)
addMissingEndDates(phase2Elements)
const byStartDate = (a, b) => new Date(a.startDate) - new Date(b.startDate)
const byStartDate = (a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
return {
phase1Elements: Object.entries(phase1Elements).sort(byStartDate),
@@ -298,8 +303,12 @@ const buildStudyRightElements = (studyRightSnapshots, groupIdToCode) => {
}
}
export const updateSISStudyRightElements = async (groupedStudyRights, moduleGroupIdToCode, createdStudyRights) => {
const studyRightElements = []
export const updateSISStudyRightElements = async (
groupedStudyRights,
moduleGroupIdToCode,
createdStudyRights: Set<string>
) => {
const studyRightElements: any[] = []
for (const group of groupedStudyRights) {
const latestSnapshot = group[0]
@@ -1,8 +1,8 @@
import { flatten, sortBy, uniqBy } from 'lodash-es'
import { rootOrgId } from '../../config.js'
import { dbConnections } from '../../db/connection.js'
import { selectFromByIds, bulkCreate, selectOneById } from '../../db/index.js'
import { Course, Teacher, Credit, CreditTeacher, CourseProvider } from '../../db/models/index.js'
import { rootOrgId } from '../../config'
import { dbConnections } from '../../db/connection'
import { selectFromByIds, bulkCreate, selectOneById } from '../../db/index'
import { Course, Teacher, Credit, CreditTeacher, CourseProvider } from '../../db/models/index'
import {
mapTeacher,
creditMapper,
@@ -10,12 +10,12 @@ import {
validAttainmentTypes,
customAttainmentTypes,
isModule,
} from '../mapper.js'
} from '../mapper'
const earlierOf = (a, b) => (new Date(a) < new Date(b) ? a : b)
const laterOf = (a, b) => (new Date(a) > new Date(b) ? a : b)
const updateTeachers = async attainments => {
const updateTeachers = async (attainments: any[]) => {
const acceptorPersonIds = flatten(
attainments.map(attainment =>
attainment.acceptor_persons
@@ -33,7 +33,7 @@ const updateTeachers = async attainments => {
export const updateAttainments = async (
attainments,
personIdToStudentNumber,
attainmentsToBeExluced,
attainmentsToBeExluced: Set<string>,
studyRightIdToEducationType
) => {
await updateTeachers(attainments)
@@ -48,7 +48,7 @@ export const updateAttainments = async (
}
return acc
},
{ cuIds: [], modGroupIds: [] }
{ cuIds: [] as string[], modGroupIds: [] as string[] }
)
const [courseUnits, modules] = await Promise.all([
@@ -74,10 +74,10 @@ export const updateAttainments = async (
)
)
const creditTeachers = []
const creditTeachers: any[] = []
const coursesToBeCreated = new Map()
const courseProvidersToBeCreated = []
const courseProvidersToBeCreated: any[] = []
// This mayhem fixes missing course_unit references for CustomCourseUnitAttainments.
const fixCustomCourseUnitAttainments = async attainments => {
@@ -90,11 +90,11 @@ export const updateAttainments = async (
if (!studyModule) {
if (att.module_group_id === 'hy-SM-89304486') {
const course = await Course.findOne({ attributes: ['id'], where: { code: '71066' }, raw: true })
return { ...att, module_group_id: course.id }
return { ...att, module_group_id: course!.id }
}
if (att.module_group_id === 'hy-SM-100017957') {
const course = await Course.findOne({ attributes: ['id'], where: { code: '523102' }, raw: true })
return { ...att, module_group_id: course.id }
return { ...att, module_group_id: course!.id }
}
const education = await selectOneById('educations', att.module_group_id.replace('DP', 'EDU'))
@@ -241,7 +241,10 @@ export const updateAttainments = async (
return attainmentIdCodeMap
}
const attainmentIdCourseCodeMapForCustomCourseUnitAttainments = attainments.reduce(findMissingCourseCodes, {})
const attainmentIdCourseCodeMapForCustomCourseUnitAttainments: Record<string, string> = attainments.reduce(
findMissingCourseCodes,
{}
)
const missingCodes = Object.values(attainmentIdCourseCodeMapForCustomCourseUnitAttainments)
const courses = await selectFromByIds('course_units', missingCodes, 'code')
@@ -280,7 +283,7 @@ export const updateAttainments = async (
studyRightIdToEducationType
)
const credits = []
const credits: any[] = []
for (const attainment of fixedAttainments) {
if (
@@ -1,15 +1,13 @@
import { readFileSync } from 'fs'
import logger from '../../utils/logger.js'
const __dir = import.meta.dirname
import logger from '../../utils/logger'
export const getAttainmentsToBeExcluded = () => {
try {
const data = readFileSync(`${__dir}/excludedPartialAttainments.csv`).toString()
if (!data) return new Set()
const data = readFileSync(`${__dirname}/excludedPartialAttainments.csv`).toString()
if (!data) return new Set<string>()
const attainmentIds = data.split('\n')
return new Set(attainmentIds)
} catch (error) {
} catch (error: any) {
logger.error({ message: 'Reading excluded attainments from csv failed', meta: error.stack })
throw error
}
@@ -1,15 +1,15 @@
import { flatten, groupBy, orderBy } from 'lodash-es'
import { bulkCreate, selectFromActiveSnapshotsByIds, selectFromByIds } from '../../db/index.js'
import { Enrollment, Student } from '../../db/models/index.js'
import logger from '../../utils/logger.js'
import { studentMapper, enrollmentMapper } from '../mapper.js'
import { getEducation, loadMapsIfNeeded } from '../shared.js'
import { updateAttainments } from './attainments.js'
import { getAttainmentsToBeExcluded } from './excludedPartialAttainments.js'
import { updateSISStudyRights, updateSISStudyRightElements } from './SISStudyRights.js'
import { updateStudyplans, findStudentsToReupdate } from './studyPlans.js'
import { dbConnections } from '../../db/connection.js'
import { bulkCreate, selectFromActiveSnapshotsByIds, selectFromByIds } from '../../db/index'
import { Enrollment, Student } from '../../db/models/index'
import logger from '../../utils/logger'
import { studentMapper, enrollmentMapper } from '../mapper'
import { getEducation, loadMapsIfNeeded } from '../shared'
import { updateAttainments } from './attainments'
import { getAttainmentsToBeExcluded } from './excludedPartialAttainments'
import { updateSISStudyRights, updateSISStudyRightElements } from './SISStudyRights'
import { updateStudyplans, findStudentsToReupdate } from './studyPlans'
import { dbConnections } from '../../db/connection'
// Accepted selection path is not available when degree programme doesn't have
// studytrack or major subject. This is a known bug on SIS and has been reported
@@ -62,7 +62,7 @@ const groupStudyrightSnapshots = studyrightSnapshots => {
const parseSnapshotDateTime = snapshots => {
const oldestFirst = snapshots
.filter(s => !!s.snapshot_date_time)
.sort((a, b) => new Date(a.snapshot_date_time) - new Date(b.snapshot_date_time))
.sort((a, b) => new Date(a.snapshot_date_time).getTime() - new Date(b.snapshot_date_time).getTime())
if (oldestFirst.length) return oldestFirst[0].snapshot_date_time
return null
}
@@ -108,7 +108,7 @@ const updateEnrollments = async (enrollments, personIdToStudentNumber, studyRigh
acc.cuIds.push(enrollment.course_unit_id)
return acc
},
{ curIds: [], cuIds: [] }
{ curIds: [] as string[], cuIds: [] as string[] }
)
const [courseUnitRealisations, courseUnits] = await Promise.all([
@@ -142,7 +142,7 @@ const updateEnrollments = async (enrollments, personIdToStudentNumber, studyRigh
}
const createModuleGroupIdToCodeMap = async studyRights => {
const groupIds = new Set()
const groupIds = new Set<string>()
for (const { accepted_selection_path } of studyRights) {
const { educationPhase1GroupId, educationPhase1ChildGroupId, educationPhase2GroupId, educationPhase2ChildGroupId } =
@@ -229,7 +229,7 @@ export const updateStudents = async (personIds, iteration = 0) => {
return res
}, {})
const logError = (error, updatedItem) => {
const logError = (error, updatedItem: string) => {
logger.error({ message: `Failed to update ${updatedItem}`, error })
}
@@ -1,14 +1,14 @@
import { flatten, flattenDeep } from 'lodash-es'
import { Op } from 'sequelize'
import { selectFromByIds, bulkCreate } from '../../db/index.js'
import { Studyplan } from '../../db/models/index.js'
import { isBaMa } from '../../utils/index.js'
import { studyplanMapper, sanitizeCourseCode } from '../mapper.js'
import { getEducation } from '../shared.js'
import { selectFromByIds, bulkCreate } from '../../db/index'
import { Studyplan } from '../../db/models/index'
import { isBaMa } from '../../utils/index'
import { studyplanMapper, sanitizeCourseCode } from '../mapper'
import { getEducation } from '../shared'
export const updateStudyplans = async (
studyplansAll,
studyplansAll: any[],
personIds,
personIdToStudentNumber,
groupedStudyRightSnapshots
@@ -31,7 +31,7 @@ export const updateStudyplans = async (
const educationHasStudyRight = Object.keys(groupedStudyRightSnapshots).reduce((acc, k) => {
const sorted = groupedStudyRightSnapshots[k]
.filter(s => new Date(s.snapshot_date_time) <= new Date())
.sort((a, b) => new Date(b.snapshot_date_time) - new Date(a.snapshot_date_time))
.sort((a, b) => new Date(b.snapshot_date_time).getTime() - new Date(a.snapshot_date_time).getTime())
if (!sorted.length) return acc
const { education_id, person_id } = sorted[0]
acc[education_id] ??= {}
@@ -42,7 +42,7 @@ export const updateStudyplans = async (
const educationStudyrights = Object.keys(groupedStudyRightSnapshots).reduce((acc, k) => {
const sorted = groupedStudyRightSnapshots[k]
.filter(s => new Date(s.snapshot_date_time) <= new Date())
.sort((a, b) => new Date(b.snapshot_date_time) - new Date(a.snapshot_date_time))
.sort((a, b) => new Date(b.snapshot_date_time).getTime() - new Date(a.snapshot_date_time).getTime())
if (!sorted.length) return acc
const { education_id, person_id, id } = sorted[0]
const educationType = getEducation(education_id)
@@ -56,9 +56,7 @@ export const updateStudyplans = async (
return acc
}, {})
const filteredPlans = studyplans.filter(
p => educationHasStudyRight[p.root_id] && educationHasStudyRight[p.root_id][p.user_id]
)
const filteredPlans = studyplans.filter(p => educationHasStudyRight[p.root_id]?.[p.user_id])
const courseUnitSelections = flatten(studyplans.map(plan => plan.course_unit_selections.map(cu => cu.courseUnitId)))
const courseUnitSelectionSubstitutedBy = flattenDeep(
@@ -129,13 +127,13 @@ export const updateStudyplans = async (
return res
}, {})
const findDegreeProgrammes = (programId, handledProgrammeIds) => {
const findDegreeProgrammes = (programId: string, handledProgrammeIds: Set<string>): string[] => {
if (programmeModuleIdToType[programId] === 'DegreeProgramme') return [programId]
if (!childModules[programId]) return []
handledProgrammeIds.add(programId)
const programmes = []
childModules[programId].forEach(child => {
const programmes: string[] = []
childModules[programId].forEach((child: string) => {
if (!handledProgrammeIds.has(child)) programmes.push(...findDegreeProgrammes(child, handledProgrammeIds))
})
return programmes
@@ -181,7 +179,7 @@ export const updateStudyplans = async (
const getAttainmentsFromAttainment = attainment => {
if (!attainment) return []
if (attainment.nodes && attainment.nodes.length)
if (attainment.nodes?.length)
return flatten(
attainment.nodes
.filter(node => node.attainmentId)
@@ -195,20 +193,20 @@ export const updateStudyplans = async (
return res
}, {})
const moduleIdToParentDegreeProgramme = {}
const mapParentDegreeProgrammes = (moduleId, degreeProgrammeId, handledModuleIds) => {
const moduleIdToParentDegreeProgramme: Record<string, string[]> = {}
const mapParentDegreeProgrammes = (moduleId: string, degreeProgrammeId: string, handledModuleIds: Set<string>) => {
moduleIdToParentDegreeProgramme[moduleId] ??= []
moduleIdToParentDegreeProgramme[moduleId].push(degreeProgrammeId)
handledModuleIds.add(moduleId)
if (!childModules[moduleId]) return
childModules[moduleId].forEach(child => {
childModules[moduleId].forEach((child: string) => {
if (!handledModuleIds.has(child)) mapParentDegreeProgrammes(child, degreeProgrammeId, handledModuleIds)
})
}
studyplans.forEach(plan => {
studyplanIdToDegreeProgrammes[plan.id].forEach(programmeId => {
studyplanIdToDegreeProgrammes[plan.id].forEach((programmeId: string) => {
mapParentDegreeProgrammes(programmeId, programmeId, new Set())
})
})
@@ -221,7 +219,7 @@ export const updateStudyplans = async (
const moduleIdToParentModuleCode = Object.entries(moduleIdToParentDegreeProgramme).reduce(
(acc, [moduleId, degreeProgrammeIds]) => {
const codes = new Set()
const codes = new Set<string>()
for (const programmeId of degreeProgrammeIds) {
codes.add(programmeModuleIdToCode[programmeId])
}
@@ -250,9 +248,7 @@ export const updateStudyplans = async (
)
const mappedPlans = flatten(
filteredPlans
.filter(p => educationHasStudyRight[p.root_id] && educationHasStudyRight[p.root_id][p.user_id])
.map(mapStudyplan)
filteredPlans.filter(p => educationHasStudyRight[p.root_id]?.[p.user_id]).map(mapStudyplan)
).filter(p => !!p)
await bulkCreate(Studyplan, mappedPlans)
}
@@ -268,9 +264,8 @@ export const findStudentsToReupdate = async (personIds, personIdToStudentNumber,
studentnumber: { [Op.in]: personIds.map(id => personIdToStudentNumber[id]) },
},
attributes: ['studentnumber'],
distinct: true,
})
const studentNumberToPersonId = Object.entries(personIdToStudentNumber).reduce((obj, cur) => {
const studentNumberToPersonId = Object.entries<any>(personIdToStudentNumber).reduce((obj, cur) => {
const [personId, studentnumber] = cur
obj[studentnumber] = personId
return obj
@@ -1,7 +1,7 @@
import { Op } from 'sequelize'
import { selectFromByIds } from '../../db/index.js'
import { SISStudyRight, SISStudyRightElement } from '../../db/models/index.js'
import { selectFromByIds } from '../../db/index'
import { SISStudyRight, SISStudyRightElement } from '../../db/models/index'
export const studentsThatNeedToBeFixed = [
{ id: 'hy-hlo-109316006', started: '2019-04-30' },
@@ -23,7 +23,7 @@ export const studentsThatNeedToBeFixed = [
{ id: 'hy-hlo-62121710', started: '2020-12-19' },
]
export const fixVarhaiskasvatusStudyRights = async studentsToBeFixed => {
export const fixVarhaiskasvatusStudyRights = async (studentsToBeFixed: string[]) => {
const students = await selectFromByIds('persons', studentsToBeFixed)
for (const student of students) {
const studentInfo = studentsThatNeedToBeFixed.find(s => s.id === student.id)
@@ -45,7 +45,7 @@ export const fixVarhaiskasvatusStudyRights = async studentsToBeFixed => {
if (studyRight) {
const bachelorElement = studyRight.SISStudyRightElements.find(element => element.code === 'KH60_001')
if (bachelorElement) {
bachelorElement.startDate = new Date(studentInfo.started)
bachelorElement.startDate = new Date(studentInfo!.started)
await bachelorElement.save()
if (bachelorElement.graduated) {
const masterElement = studyRight.SISStudyRightElements.find(element => element.code === 'MH60_001')
@@ -1,13 +1,13 @@
import { maxBy } from 'lodash-es'
export const getLatestSnapshot = entities => {
export const getLatestSnapshot = (entities: any[]) => {
return maxBy(
entities.filter(entity => entity.document_state !== 'DELETED'),
entity => entity.modification_ordinal
)
}
export const getActiveSnapshot = entities => {
export const getActiveSnapshot = (entities: any[]) => {
const now = new Date()
return maxBy(
entities
@@ -17,7 +17,7 @@ export const getActiveSnapshot = entities => {
)
}
export const getLatestActiveSnapshot = entities => {
export const getLatestActiveSnapshot = (entities: any[]) => {
return maxBy(
entities.filter(entity => entity.document_state == 'ACTIVE'),
entity => entity.modification_ordinal
@@ -1,25 +1,23 @@
import { format as winstonFormat, transports as winstonTransports, createLogger } from 'winston'
import LokiTransport from 'winston-loki'
import WinstonSentry from 'winston-transport-sentry-node'
import SentryTransport from 'winston-transport-sentry-node'
import { isDev, isStaging, isProduction, runningInCI, SENTRY_DSN } from '../config.js'
const Sentry = WinstonSentry.default
import { isDev, isStaging, isProduction, runningInCI, SENTRY_DSN } from '../config'
const { colorize, combine, timestamp, printf, uncolorize } = winstonFormat
const transports = []
const transports: any[] = []
if (isProduction && !isStaging && !runningInCI && SENTRY_DSN) {
transports.push(new Sentry({ level: 'error' }))
transports.push(new SentryTransport({ level: 'error' }))
}
const devFormat = printf(
({ timestamp, level, message, error, ...rest }) =>
({ timestamp, level, message, error, ...rest }: any) =>
`${timestamp} ${level}: ${message}${error ? ` ${error?.stack}` : ''}${rest ? ` ${JSON.stringify(rest)}` : ''}`
)
const prodFormat = printf(({ timestamp, level, message, error, ...rest }) => {
const prodFormat = printf(({ timestamp, level, message, error, ...rest }: any) => {
const log = { timestamp, level, message, ...rest }
if (error) {
log.error = error?.stack
@@ -1,11 +1,8 @@
import { createRequire } from 'module'
const legacyRequire = createRequire(import.meta.url)
const redisLock = legacyRequire('redis-lock')
import { createClient } from 'redis'
import redisLock from 'redis-lock'
import { REDIS_HOST, REDIS_PORT } from '../config.js'
import logger from './logger.js'
import { REDIS_HOST, REDIS_PORT } from '../config'
import logger from './logger'
export const redisClient = createClient({
url: `redis://${REDIS_HOST}:${REDIS_PORT}`,
@@ -1,8 +1,8 @@
import { Worker } from 'bullmq'
import { REDIS_HOST, REDIS_PORT } from './config.js'
import processor from './processor.js'
import logger from './utils/logger.js'
import { REDIS_HOST, REDIS_PORT } from './config'
import processor from './processor'
import logger from './utils/logger'
const connection = {
host: REDIS_HOST,
@@ -16,7 +16,7 @@ worker.on('completed', job => {
message: `Job ${job.id} completed`,
type: job.name,
count: job.data?.length,
timems: job.finishedOn - job.processedOn,
timems: job.finishedOn! - job.processedOn!,
})
})
@@ -26,7 +26,7 @@ worker.on('error', error => {
})
worker.on('failed', (job, error) => {
logger.error(`Job ${job.id} failed. ${error.stack ?? ''}`)
logger.error(`Job ${job?.id} failed. ${error.stack ?? ''}`)
})
worker.on('active', job => {
@@ -24,7 +24,7 @@ const { knexMock, bulkCreateMock, selectFromByIdsMock, selectOneByIdMock } = vi.
}
const knexMock = Object.assign(
vi.fn(table => makeChainableQuery(tableResults[table] ?? [])),
vi.fn((table: string) => makeChainableQuery(tableResults[table] ?? [])),
{ select: () => makeChainableQuery([]) }
)
@@ -36,15 +36,15 @@ const { knexMock, bulkCreateMock, selectFromByIdsMock, selectOneByIdMock } = vi.
}
})
vi.mock('@/db/connection.js', () => ({ dbConnections: { knex: knexMock } }))
vi.mock('@/db/connection', () => ({ dbConnections: { knex: knexMock } }))
vi.mock('@/db/index.js', () => ({
vi.mock('@/db/index', () => ({
selectFromByIds: selectFromByIdsMock,
selectOneById: selectOneByIdMock,
bulkCreate: bulkCreateMock,
}))
vi.mock('@/db/models/index.js', () => ({
vi.mock('@/db/models/index', () => ({
Course: { name: 'Course', findOne: vi.fn() },
Teacher: { name: 'Teacher', findOne: vi.fn() },
Credit: { name: 'Credit' },
@@ -52,13 +52,13 @@ vi.mock('@/db/models/index.js', () => ({
CourseProvider: { name: 'CourseProvider', findOne: vi.fn() },
}))
vi.mock('@/utils/logger.js', () => ({
vi.mock('@/utils/logger', () => ({
default: { error: vi.fn(), info: vi.fn(), debug: vi.fn(), warn: vi.fn() },
}))
// updateAttainments builds its credit rows via mapper.js's real creditMapper, which in turn reads
// from shared.js's Redis-backed lookup maps - mocked here the same way as in mapper.test.js.
vi.mock('@/updater/shared.js', () => ({
vi.mock('@/updater/shared', () => ({
getSemesterByDate: vi.fn(() => ({ semestercode: 100, composite: '2020-2021-fall' })),
getGrade: vi.fn(() => ({ value: '5', passed: true })),
getUniOrgId: vi.fn(() => 'uni-org1'),
@@ -67,8 +67,8 @@ vi.mock('@/updater/shared.js', () => ({
educationTypeToExtentcode: {},
}))
import { Course } from '@/db/models/index.js'
import { updateAttainments } from '@/updater/updateStudents/attainments.js'
import { Course } from '@/db/models/index'
import { updateAttainments } from '@/updater/updateStudents/attainments'
describe('updateAttainments', () => {
beforeEach(() => {
@@ -76,7 +76,8 @@ describe('updateAttainments', () => {
})
/** Get what calls were directed to the wanted model by bulkCreate */
const findBulkCreateCall = modelName => bulkCreateMock.mock.calls.find(([model]) => model.name === modelName)
const findBulkCreateCall = (modelName: string) =>
bulkCreateMock.mock.calls.find(([model]) => model.name === modelName)!
it('maps a plain course unit attainment into a Credit bulkCreate call', async () => {
const attainment = {
@@ -150,7 +151,7 @@ describe('updateAttainments', () => {
})
it('updates custom course unit with fields from a new attainment', async () => {
Course.findOne.mockResolvedValueOnce({
vi.mocked(Course.findOne).mockResolvedValueOnce({
id: 'XYZ-123',
groupId: null, // Group id missing
code: 'XYZ-123',
@@ -162,7 +163,7 @@ describe('updateAttainments', () => {
minAttainmentDate: '2019-01-01',
substitutionGroups: [],
courseUnitType: 'type1',
})
} as any)
const attainment = {
id: 'att-custom-2',
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { courseProviderMapper } from '@/updater/mapper.js'
import { courseProviderMapper } from '@/updater/mapper'
describe('courseProviderMapper', () => {
it('maps organisation shares to a course provider row', () => {
@@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import './initTests.js'
import './initTests'
import { creditMapper } from '@/updater/mapper.js'
import { getCreditTypeCodeFromAttainment, getGrade, getSemesterByDate, getUniOrgId } from '@/updater/shared.js'
import { creditMapper } from '@/updater/mapper'
import { getCreditTypeCodeFromAttainment, getGrade, getSemesterByDate, getUniOrgId } from '@/updater/shared'
describe('creditMapper', () => {
const baseAttainment = {
@@ -40,10 +40,10 @@ describe('creditMapper', () => {
)
it('maps attainment correctly into credit', () => {
getUniOrgId.mockReturnValue('uni-org1')
getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
getGrade.mockReturnValue({ value: '5', passed: true })
getCreditTypeCodeFromAttainment.mockReturnValue('PASSED')
vi.mocked(getUniOrgId).mockReturnValue('uni-org1')
vi.mocked(getSemesterByDate).mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
vi.mocked(getGrade).mockReturnValue({ value: '5', passed: true })
vi.mocked(getCreditTypeCodeFromAttainment).mockReturnValue('PASSED' as any)
const result = mapCredit(lookupMaps())(baseAttainment)
@@ -68,15 +68,15 @@ describe('creditMapper', () => {
})
it('returns null when there is no target semester for the attainment date', () => {
getUniOrgId.mockReturnValue('uni-org1')
getSemesterByDate.mockReturnValue(undefined)
vi.mocked(getUniOrgId).mockReturnValue('uni-org1')
vi.mocked(getSemesterByDate).mockReturnValue(undefined)
expect(mapCredit(lookupMaps())(baseAttainment)).toBeNull()
})
it('returns null when the course code cannot be resolved', () => {
getUniOrgId.mockReturnValue('uni-org1')
getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
vi.mocked(getUniOrgId).mockReturnValue('uni-org1')
vi.mocked(getSemesterByDate).mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
const result = mapCredit(lookupMaps({ courseUnitIdToCourseCode: {} }))(baseAttainment)
@@ -84,21 +84,21 @@ describe('creditMapper', () => {
})
it('marks open university attainments via the AY course code prefix', () => {
getUniOrgId.mockReturnValue('uni-org1')
getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
getGrade.mockReturnValue({ value: '5', passed: true })
getCreditTypeCodeFromAttainment.mockReturnValue('PASSED')
vi.mocked(getUniOrgId).mockReturnValue('uni-org1')
vi.mocked(getSemesterByDate).mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
vi.mocked(getGrade).mockReturnValue({ value: '5', passed: true })
vi.mocked(getCreditTypeCodeFromAttainment).mockReturnValue('PASSED' as any)
const result = mapCredit(lookupMaps({ courseUnitIdToCourseCode: { cu1: 'AY123' } }))(baseAttainment)
expect(result.is_open).toBe(true)
expect(result!.is_open).toBe(true)
})
it('marks open university attainments via the study right education type', () => {
getUniOrgId.mockReturnValue('uni-org1')
getSemesterByDate.mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
getGrade.mockReturnValue({ value: '5', passed: true })
getCreditTypeCodeFromAttainment.mockReturnValue('PASSED')
vi.mocked(getUniOrgId).mockReturnValue('uni-org1')
vi.mocked(getSemesterByDate).mockReturnValue({ semestercode: 100, composite: '2020-2021-fall' })
vi.mocked(getGrade).mockReturnValue({ value: '5', passed: true })
vi.mocked(getCreditTypeCodeFromAttainment).mockReturnValue('PASSED' as any)
const attainment = { ...baseAttainment, study_right_id: 'sr1' }
const result = mapCredit(
@@ -107,7 +107,7 @@ describe('creditMapper', () => {
})
)(attainment)
expect(result.is_open).toBe(true)
expect(result!.is_open).toBe(true)
})
it('returns null with missing organization', () => {
@@ -1,9 +1,9 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import './initTests.js'
import './initTests'
import { enrollmentMapper } from '@/updater/mapper.js'
import { getSemesterByDate } from '@/updater/shared.js'
import { enrollmentMapper } from '@/updater/mapper'
import { getSemesterByDate } from '@/updater/shared'
describe('enrollmentMapper', () => {
const baseEnrollment = {
@@ -16,7 +16,7 @@ describe('enrollmentMapper', () => {
study_right_id: null,
}
const mapEnrollment = (overrides = {}) =>
const mapEnrollment = (overrides: any = {}) =>
enrollmentMapper(
overrides.personIdToStudentNumber ?? { p1: '014111111' },
overrides.courseUnitIdToCourseUnit ?? { cu1: { code: 'TKT10', group_id: 'grp1' } },
@@ -33,7 +33,7 @@ describe('enrollmentMapper', () => {
})
it('maps an enrollment correctly', () => {
getSemesterByDate.mockReturnValue({ semestercode: 5, composite: 'sem5' })
vi.mocked(getSemesterByDate).mockReturnValue({ semestercode: 5, composite: 'sem5' })
expect(mapEnrollment()(baseEnrollment)).toEqual({
id: 'e1',
@@ -8,11 +8,11 @@ import { vi } from 'vitest'
// this directory. Tests that need per-case control over a shared.js lookup (e.g. creditMapper,
// enrollmentMapper) can still `import { getSemesterByDate, ... } from '.../updater/shared.js'`
// afterwards and call `.mockReturnValue(...)` on it - it's the same mocked function instance.
vi.mock('@/utils/logger.js', () => ({
vi.mock('@/utils/logger', () => ({
default: { error: vi.fn(), info: vi.fn(), debug: vi.fn(), warn: vi.fn() },
}))
vi.mock('@/updater/shared.js', () => ({
vi.mock('@/updater/shared', () => ({
getSemesterByDate: vi.fn(),
getGrade: vi.fn(),
getUniOrgId: vi.fn(),
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { isModule } from '@/updater/mapper.js'
import { isModule } from '@/updater/mapper'
describe('isModule', () => {
it('is true for module attainment types', () => {
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { mapCourseType } from '@/updater/mapper.js'
import { mapCourseType } from '@/updater/mapper'
describe('mapCourseType', () => {
it('maps a study level to a course type row', () => {
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { mapCurriculumPeriod } from '@/updater/mapper.js'
import { mapCurriculumPeriod } from '@/updater/mapper'
describe('mapCurriculumPeriod', () => {
it('maps a curriculum period correctly', () => {
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { mapStudyrightExtent } from '@/updater/mapper.js'
import { mapStudyrightExtent } from '@/updater/mapper'
describe('mapStudyrightExtent', () => {
it('resolves the extent code from educationTypeToExtentcode', () => {
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { mapTeacher } from '@/updater/mapper.js'
import { mapTeacher } from '@/updater/mapper'
describe('mapTeacher', () => {
it('combines last and first names when present', () => {
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { sanitizeCourseCode } from '@/updater/mapper.js'
import { sanitizeCourseCode } from '@/updater/mapper'
describe('sanitizeCourseCode', () => {
it('returns null for falsy input', () => {
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { studentMapper } from '@/updater/mapper.js'
import { studentMapper } from '@/updater/mapper'
describe('studentMapper', () => {
const baseStudent = {
@@ -74,9 +74,9 @@ describe('studentMapper', () => {
const result = studentMapper(attainments, [], new Set())(baseStudent)
expect(result.creditcount).toBe(10)
expect(result.studentnumber).toBe('014999999')
expect(result.abbreviatedname).toBe('Doe Jane')
expect(result.dateofuniversityenrollment).toBeNull()
expect(result!.creditcount).toBe(10)
expect(result!.studentnumber).toBe('014999999')
expect(result!.abbreviatedname).toBe('Doe Jane')
expect(result!.dateofuniversityenrollment).toBeNull()
})
})
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { termRegistrationTypeToEnrollmenttype } from '@/updater/mapper.js'
import { termRegistrationTypeToEnrollmenttype } from '@/updater/mapper'
describe('termRegistrationTypeToEnrollmenttype', () => {
it('maps known types and falls back to 3', () => {
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { educationTypeToExtentcode } from '@/updater/shared.js'
import { educationTypeToExtentcode } from '@/updater/shared'
describe('educationTypeToExtentcode', () => {
it('maps known education types to their extent code', () => {
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest'
import './initTests.js'
import './initTests'
import { getCreditTypeCodeFromAttainment } from '@/updater/shared.js'
import { getCreditTypeCodeFromAttainment } from '@/updater/shared'
describe('getCreditTypeCodeFromAttainment', () => {
it('is FAILED when the grade did not pass', () => {
@@ -5,17 +5,17 @@ import { vi } from 'vitest'
//
// Import this module first - before importing anything from shared.js - in every test file in
// this directory.
vi.mock('@/utils/redis.js', () => ({
vi.mock('@/utils/redis', () => ({
redisClient: { get: vi.fn(), set: vi.fn(), connect: vi.fn(), on: vi.fn() },
lock: vi.fn(),
}))
vi.mock('@/db/index.js', () => ({
vi.mock('@/db/index', () => ({
selectAllFrom: vi.fn(),
selectAllFromSnapshots: vi.fn(),
}))
vi.mock('@/db/models/index.js', () => ({
vi.mock('@/db/models/index', () => ({
Semester: {},
Organization: {},
CREDIT_TYPE_CODES: { PASSED: 4, FAILED: 10, IMPROVED: 7, APPROVED: 9 },
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ES2024",
"lib": ["ES2024"],
"allowJs": true,
"module": "nodenext",
"moduleResolution": "nodenext",
"outDir": "dist",
"paths": {
"@/*": ["./src/*"]
},
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": false,
"noErrorTruncation": true,
"sourceMap": true,
"sourceRoot": "/",
"inlineSources": true,
"resolveJsonModule": true
},
"include": ["src", "tests", "vitest.config.mts"]
}