diff --git a/.docker/.manifests/apps__docs__package.json b/.docker/.manifests/apps__docs__package.json index 8514c44..405ddd0 100644 --- a/.docker/.manifests/apps__docs__package.json +++ b/.docker/.manifests/apps__docs__package.json @@ -39,6 +39,7 @@ "postcss": "^8.4.47", "tailwindcss": "^4.0.0", "typescript": "^5.5.3", + "unified": "^11.0.5", "vite": "^5.4.3" } } diff --git a/.docker/.manifests/apps__server__package.json b/.docker/.manifests/apps__server__package.json deleted file mode 100644 index 5e7f493..0000000 --- a/.docker/.manifests/apps__server__package.json +++ /dev/null @@ -1,119 +0,0 @@ -{ - "name": "@cv/server", - "version": "0.0.0", - "private": true, - "type": "commonjs", - "scripts": { - "build": "tsup", - "postbuild": "sh scripts/post-build.sh", - "start": "node dist/main.js", - "dev": "nodemon --watch src -e ts --exec \"ts-node -r tsconfig-paths/register src/main.ts\"", - "lint": "biome check .", - "lint:fix": "biome check --write .", - "typecheck": "tsc -p tsconfig.build.json --noEmit", - "test": "jest --config ./test/jest-unit.json", - "test:unit": "jest --config ./test/jest-unit.json", - "test:e2e": "jest --config ./test/jest-e2e.json", - "e2e:coverage:c8:start": "npm run prisma:deploy && npm run build && npx c8 -r text -r lcov node dist/main.js", - "e2e:coverage:c8:report": "npx c8 report -r text -r lcov", - "e2e:coverage:c8": "E2E_EXTERNAL_URL=http://localhost:3000 npm run test:e2e && npm run e2e:coverage:c8:report", - "e2e:coverage:c8:stop": "pkill -f 'node dist/main.js' || true", - "e2e:coverage:all": "node src/scripts/e2e-coverage-all.cjs", - "prepare": "prisma generate || true", - "prisma:generate": "prisma generate", - "prisma:migrate": "prisma migrate dev", - "prisma:deploy": "prisma migrate deploy", - "prisma:studio": "prisma studio", - "seed": "ts-node -r tsconfig-paths/register src/scripts/seed.ts", - "seed:test": "ts-node -r tsconfig-paths/register src/scripts/seed-test.ts", - "test:integration": "vitest run --config vitest.config.ts", - "test:integration:watch": "vitest --config vitest.config.ts", - "test:integration:ui": "vitest --ui --config vitest.config.ts" - }, - "dependencies": { - "@apollo/server": "^4.11.0", - "@cv/ai-parser": "*", - "@cv/ai-provider": "*", - "@cv/auth": "*", - "@cv/cv-renderer": "*", - "@cv/file-storage": "*", - "@cv/file-upload": "*", - "@cv/system": "*", - "@cv/utils": "*", - "@nestjs/apollo": "^12.2.2", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", - "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@nestjs/jwt": "^10.2.0", - "@nestjs/mapped-types": "^2.1.0", - "@nestjs/passport": "^10.0.3", - "@nestjs/platform-express": "^10.4.7", - "@prisma/adapter-pg": "^7.1.0", - "@prisma/client": "^7.1.0", - "@riotbyte-com/nest-service-locator": "0.3.0-rc.4", - "@riotbyte-com/project-q-core": "1.0.19-rc.3", - "@riotbyte-com/project-q-nestjs": "1.0.19-rc.3", - "@riotbyte-com/project-q-prisma": "1.0.19-rc.3", - "@types/cookie-parser": "^1.4.10", - "@types/handlebars": "^4.0.40", - "@types/multer": "^2.0.0", - "apollo-server-express": "^3.13.0", - "bcryptjs": "^2.4.3", - "canvas": "^3.1.0", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.0", - "cookie-parser": "^1.4.7", - "dataloader": "^2.2.3", - "express": "^4.21.2", - "graphql": "^16.12.0", - "graphql-scalars": "^1.23.0", - "graphql-type-json": "^0.3.2", - "handlebars": "^4.7.8", - "mammoth": "^1.11.0", - "multer": "^2.0.2", - "nest-commander": "^3.16.0", - "nestjs-zod": "^5.1.1", - "passport": "^0.7.0", - "passport-jwt": "^4.0.1", - "passport-local": "^1.0.0", - "pdf-parse": "^1.1.4", - "pdf2json": "^3.1.4", - "pdfjs-dist": "^3.11.174", - "pg": "^8.16.3", - "reflect-metadata": "^0.2.2", - "resend": "^6.5.2", - "rxjs": "^7.8.1", - "tesseract.js": "^5.1.1", - "zod": "^4.3.6" - }, - "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@cv/biome-config": "*", - "@cv/tsconfig": "*", - "@faker-js/faker": "^9.9.0", - "@nestjs/testing": "^10.4.7", - "@swc/core": "^1.15.13", - "@types/bcryptjs": "^2.4.6", - "@types/express": "^5.0.0", - "@types/jest": "^29.5.12", - "@types/node": "^22.7.5", - "@types/passport-jwt": "^4.0.1", - "@types/passport-local": "^1.0.38", - "@types/pdf-parse": "^1.1.5", - "@types/pg": "^8.15.6", - "@vitest/ui": "^4.0.16", - "jest": "^29.7.0", - "nodemon": "^3.1.7", - "prisma": "^7.1.0", - "supertest": "^6.3.4", - "ts-jest": "^29.1.2", - "ts-node": "^10.9.2", - "tsconfig-paths": "^4.2.0", - "tslib": "^2.8.0", - "tsup": "^8.5.1", - "typescript": "^5.6.3", - "vitest": "^4.0.16" - } -} diff --git a/.docker/.manifests/apps__worker__package.json b/.docker/.manifests/apps__worker__package.json index 063c5cd..342b109 100644 --- a/.docker/.manifests/apps__worker__package.json +++ b/.docker/.manifests/apps__worker__package.json @@ -12,11 +12,12 @@ "lint:fix": "biome check --write ." }, "dependencies": { + "@cv/core": "workspace:*", "@cv/file-storage": "*", - "@cv/system": "*", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", + "@cv/handlers": "workspace:*", + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18", "@nestjs/event-emitter": "^3.0.1", "@riotbyte-com/nest-service-locator": "0.3.0-rc.4", "@riotbyte-com/project-q-core": "1.0.19-rc.3", @@ -31,7 +32,9 @@ "zod": "^4.3.6" }, "devDependencies": { + "@aws-sdk/client-s3": "^3.800.0", "@cv/tsconfig": "*", + "@types/pg": "^8.15.0", "@swc/core": "^1.15.13", "@types/node": "^22.7.5", "nodemon": "^3.1.7", diff --git a/.docker/.manifests/packages__ai-parser__package.json b/.docker/.manifests/packages__ai-parser__package.json index d7ef9fe..47a5ae8 100644 --- a/.docker/.manifests/packages__ai-parser__package.json +++ b/.docker/.manifests/packages__ai-parser__package.json @@ -6,9 +6,9 @@ "types": "./src/index.ts", "scripts": { "typecheck": "tsc --noEmit", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage" + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@cv/ai-provider": "*", @@ -20,10 +20,8 @@ }, "devDependencies": { "@cv/tsconfig": "*", - "@types/jest": "^29.5.0", "@types/node": "^22.7.5", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^4.0.0" } } diff --git a/.docker/.manifests/packages__ai-provider__package.json b/.docker/.manifests/packages__ai-provider__package.json index 4854bb0..d4bb466 100644 --- a/.docker/.manifests/packages__ai-provider__package.json +++ b/.docker/.manifests/packages__ai-provider__package.json @@ -6,10 +6,10 @@ "types": "./src/index.ts", "scripts": { "typecheck": "tsc --noEmit", - "test": "jest", - "test:watch": "jest --watch", - "test:integration": "jest --testPathPattern='.integration.test.ts$' --runInBand", - "test:all": "jest --testPathIgnorePatterns=[]" + "test": "vitest run", + "test:watch": "vitest", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:all": "vitest run --config vitest.integration.config.ts" }, "dependencies": { "@nestjs/common": "^11.1.3", @@ -22,10 +22,8 @@ }, "devDependencies": { "@cv/tsconfig": "*", - "@types/jest": "^29.5.12", "@types/node": "^22.7.5", - "jest": "^29.7.0", - "ts-jest": "^29.1.2", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^4.0.0" } } diff --git a/.docker/.manifests/packages__auth__package.json b/.docker/.manifests/packages__auth__package.json index ad1aaaa..61faa66 100644 --- a/.docker/.manifests/packages__auth__package.json +++ b/.docker/.manifests/packages__auth__package.json @@ -4,52 +4,26 @@ "private": true, "main": "./src/index.ts", "types": "./src/index.ts", - "exports": { - ".": { - "require": "./src/index.ts", - "import": "./src/index.ts", - "types": "./src/index.ts" - } - }, - "files": [ - "src/" - ], "scripts": { - "lint": "biome check .", - "lint:fix": "biome check --write .", - "typecheck": "tsc -b" + "typecheck": "tsc --noEmit" }, "dependencies": { - "@cv/system": "*", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", - "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@nestjs/jwt": "^10.2.0", - "bcryptjs": "^2.4.3", - "graphql": "^16.12.0", - "reflect-metadata": "^0.2.2", - "zod": "^4.3.6" + "@cv/core": "workspace:*", + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18", + "@nestjs/graphql": "^13.2.5", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "express": "^5.1.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@cv/biome-config": "*", "@cv/tsconfig": "*", - "@types/bcryptjs": "^2.4.0", "@types/express": "^5.0.0", - "@types/express-serve-static-core": "^5.0.0", - "typescript": "^5.6.3" - }, - "peerDependencies": { - "@cv/system": "*", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", - "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@nestjs/jwt": "^10.2.0", - "bcryptjs": "^2.4.3", - "graphql": "^16.12.0" + "@types/passport-jwt": "^4.0.1", + "typescript": "^5.3.3" } } diff --git a/.docker/.manifests/packages__file-storage__package.json b/.docker/.manifests/packages__file-storage__package.json index e95932c..aa3786a 100644 --- a/.docker/.manifests/packages__file-storage__package.json +++ b/.docker/.manifests/packages__file-storage__package.json @@ -21,9 +21,9 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.800.0", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7" + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18" }, "devDependencies": { "@biomejs/biome": "^2.2.6", @@ -32,8 +32,8 @@ "typescript": "^5.6.3" }, "peerDependencies": { - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7" + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18" } } diff --git a/.docker/.manifests/packages__file-upload__package.json b/.docker/.manifests/packages__file-upload__package.json index d709aaa..1e92687 100644 --- a/.docker/.manifests/packages__file-upload__package.json +++ b/.docker/.manifests/packages__file-upload__package.json @@ -6,7 +6,8 @@ "types": "./src/index.ts", "scripts": { "typecheck": "tsc --noEmit", - "test": "jest" + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@nestjs/common": "^11.1.3", @@ -23,11 +24,9 @@ }, "devDependencies": { "@cv/tsconfig": "*", - "@types/jest": "^29.5.0", "@types/node": "^22.7.5", "@types/pdf-parse": "^1.1.5", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^4.0.0" } } diff --git a/.docker/.manifests/packages__system__package.json b/.docker/.manifests/packages__system__package.json deleted file mode 100644 index 0870ae2..0000000 --- a/.docker/.manifests/packages__system__package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@cv/system", - "version": "0.0.0", - "private": true, - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": { - "require": "./src/index.ts", - "import": "./src/index.ts", - "types": "./src/index.ts" - } - }, - "files": [ - "src/" - ], - "scripts": { - "lint": "biome check .", - "lint:fix": "biome check --write .", - "typecheck": "tsc -b" - }, - "dependencies": { - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", - "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@prisma/adapter-pg": "^7.1.0", - "@prisma/client": "^7.1.0", - "dataloader": "^2.2.3", - "graphql": "^16.12.0", - "handlebars": "^4.7.8", - "pg": "^8.16.3", - "reflect-metadata": "^0.2.2", - "resend": "^6.5.2" - }, - "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@cv/biome-config": "*", - "@cv/tsconfig": "*", - "@types/pg": "^8.15.0", - "typescript": "^5.6.3" - }, - "peerDependencies": { - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", - "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@prisma/adapter-pg": "^7.1.0", - "@prisma/client": "^7.1.0", - "graphql": "^16.12.0", - "handlebars": "^4.7.8", - "pg": "^8.16.3", - "resend": "^6.5.2" - } -} diff --git a/.docker/server.Dockerfile b/.docker/api.Dockerfile similarity index 89% rename from .docker/server.Dockerfile rename to .docker/api.Dockerfile index a292fbb..2d29a66 100644 --- a/.docker/server.Dockerfile +++ b/.docker/api.Dockerfile @@ -29,10 +29,10 @@ RUN pnpm install --frozen-lockfile # Layer 4: source code (changes frequently) COPY packages/ ./packages/ -COPY apps/server/ ./apps/server/ +COPY apps/api/ ./apps/api/ # Generate Prisma client -WORKDIR /app/apps/server +WORKDIR /app/packages/core RUN pnpm exec prisma generate EXPOSE 3000 @@ -63,9 +63,9 @@ RUN echo "//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}" >> .npmrc RUN pnpm install --frozen-lockfile COPY packages/ ./packages/ -COPY apps/server/ ./apps/server/ +COPY apps/api/ ./apps/api/ -WORKDIR /app/apps/server +WORKDIR /app/packages/core RUN pnpm exec prisma generate RUN pnpm build @@ -94,12 +94,12 @@ RUN echo "//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}" >> .npmrc RUN pnpm install --frozen-lockfile # Copy Prisma schema + config, generate client in prod node_modules context -COPY apps/server/prisma/ ./apps/server/prisma/ -COPY apps/server/prisma.config.ts ./apps/server/prisma.config.ts -RUN cd apps/server && pnpm exec prisma generate +COPY packages/core/prisma/ ./packages/core/prisma/ +COPY packages/core/prisma.config.ts ./packages/core/prisma.config.ts +RUN cd packages/core && pnpm exec prisma generate # Copy bundled output (single file, @cv/* packages inlined) -COPY --from=builder /app/apps/server/dist/ ./apps/server/dist/ +COPY --from=builder /app/apps/api/dist/ ./apps/api/dist/ # Remove auth token from final image RUN sed -i '/_authToken/d' .npmrc @@ -107,5 +107,5 @@ RUN sed -i '/_authToken/d' .npmrc ENV NODE_ENV=production EXPOSE 3000 -WORKDIR /app/apps/server +WORKDIR /app/apps/api CMD ["node", "dist/main.js"] diff --git a/.docker/worker.Dockerfile b/.docker/worker.Dockerfile index 35a6b2b..5afa9e8 100644 --- a/.docker/worker.Dockerfile +++ b/.docker/worker.Dockerfile @@ -23,13 +23,13 @@ RUN pnpm install --frozen-lockfile # Layer 4: source code COPY packages/tsconfig/ ./packages/tsconfig/ -COPY packages/system/ ./packages/system/ +COPY packages/core/ ./packages/core/ COPY packages/file-storage/ ./packages/file-storage/ -COPY apps/server/prisma/ ./apps/server/prisma/ +COPY packages/core/prisma/ ./packages/core/prisma/ COPY apps/worker/ ./apps/worker/ -# Generate Prisma client (@cv/system depends on @prisma/client) -RUN cd apps/server && pnpm exec prisma generate +# Generate Prisma client (@cv/core depends on @prisma/client) +RUN cd packages/core && pnpm exec prisma generate # Install Chromium + system deps via Playwright RUN cd apps/worker && pnpm exec playwright install --with-deps chromium @@ -57,12 +57,12 @@ RUN echo "//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}" >> .npmrc RUN pnpm install --frozen-lockfile COPY packages/tsconfig/ ./packages/tsconfig/ -COPY packages/system/ ./packages/system/ +COPY packages/core/ ./packages/core/ COPY packages/file-storage/ ./packages/file-storage/ -COPY apps/server/prisma/ ./apps/server/prisma/ +COPY packages/core/prisma/ ./packages/core/prisma/ COPY apps/worker/ ./apps/worker/ -RUN cd apps/server && pnpm exec prisma generate +RUN cd packages/core && pnpm exec prisma generate WORKDIR /app/apps/worker RUN pnpm build diff --git a/.gitignore b/.gitignore index 4938ed3..46a6dd2 100644 --- a/.gitignore +++ b/.gitignore @@ -78,6 +78,9 @@ apps/server/test/assets/* # Coverage reports (outside coverage/) **/coverage-unit/ +# Nx daemon cache +**/.nx/ + # Worktrees .worktree/ .worktrees/ diff --git a/apps/server/eng.traineddata b/apps/api/eng.traineddata similarity index 100% rename from apps/server/eng.traineddata rename to apps/api/eng.traineddata diff --git a/apps/server/package.json b/apps/api/package.json similarity index 76% rename from apps/server/package.json rename to apps/api/package.json index 5e7f493..b98295d 100644 --- a/apps/server/package.json +++ b/apps/api/package.json @@ -1,5 +1,5 @@ { - "name": "@cv/server", + "name": "@cv/api", "version": "0.0.0", "private": true, "type": "commonjs", @@ -11,19 +11,18 @@ "lint": "biome check .", "lint:fix": "biome check --write .", "typecheck": "tsc -p tsconfig.build.json --noEmit", - "test": "jest --config ./test/jest-unit.json", - "test:unit": "jest --config ./test/jest-unit.json", - "test:e2e": "jest --config ./test/jest-e2e.json", + "test": "vitest run", + "test:unit": "vitest run", "e2e:coverage:c8:start": "npm run prisma:deploy && npm run build && npx c8 -r text -r lcov node dist/main.js", "e2e:coverage:c8:report": "npx c8 report -r text -r lcov", "e2e:coverage:c8": "E2E_EXTERNAL_URL=http://localhost:3000 npm run test:e2e && npm run e2e:coverage:c8:report", "e2e:coverage:c8:stop": "pkill -f 'node dist/main.js' || true", "e2e:coverage:all": "node src/scripts/e2e-coverage-all.cjs", - "prepare": "prisma generate || true", - "prisma:generate": "prisma generate", - "prisma:migrate": "prisma migrate dev", - "prisma:deploy": "prisma migrate deploy", - "prisma:studio": "prisma studio", + "prepare": "cd ../../packages/core && prisma generate || true", + "prisma:generate": "cd ../../packages/core && prisma generate", + "prisma:migrate": "cd ../../packages/core && prisma migrate dev", + "prisma:deploy": "cd ../../packages/core && prisma migrate deploy", + "prisma:studio": "cd ../../packages/core && prisma studio", "seed": "ts-node -r tsconfig-paths/register src/scripts/seed.ts", "seed:test": "ts-node -r tsconfig-paths/register src/scripts/seed-test.ts", "test:integration": "vitest run --config vitest.config.ts", @@ -31,25 +30,26 @@ "test:integration:ui": "vitest --ui --config vitest.config.ts" }, "dependencies": { - "@apollo/server": "^4.11.0", + "@apollo/server": "^5.0.0", + "@as-integrations/express5": "^1.1.2", "@cv/ai-parser": "*", "@cv/ai-provider": "*", - "@cv/auth": "*", + "@cv/auth": "workspace:*", + "@cv/core": "workspace:*", "@cv/cv-renderer": "*", "@cv/file-storage": "*", "@cv/file-upload": "*", - "@cv/system": "*", "@cv/utils": "*", - "@nestjs/apollo": "^12.2.2", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", + "@nestjs/apollo": "^13.2.5", + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18", "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@nestjs/jwt": "^10.2.0", + "@nestjs/graphql": "^13.2.5", + "@nestjs/jwt": "^11.0.2", "@nestjs/mapped-types": "^2.1.0", - "@nestjs/passport": "^10.0.3", - "@nestjs/platform-express": "^10.4.7", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.1.18", "@prisma/adapter-pg": "^7.1.0", "@prisma/client": "^7.1.0", "@riotbyte-com/nest-service-locator": "0.3.0-rc.4", @@ -59,14 +59,13 @@ "@types/cookie-parser": "^1.4.10", "@types/handlebars": "^4.0.40", "@types/multer": "^2.0.0", - "apollo-server-express": "^3.13.0", "bcryptjs": "^2.4.3", "canvas": "^3.1.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.7", "dataloader": "^2.2.3", - "express": "^4.21.2", + "express": "^5.1.0", "graphql": "^16.12.0", "graphql-scalars": "^1.23.0", "graphql-type-json": "^0.3.2", @@ -89,26 +88,25 @@ "zod": "^4.3.6" }, "devDependencies": { + "@aws-sdk/client-s3": "^3.800.0", "@biomejs/biome": "^2.2.6", "@cv/biome-config": "*", "@cv/tsconfig": "*", "@faker-js/faker": "^9.9.0", - "@nestjs/testing": "^10.4.7", + "@nestjs/testing": "^11.1.18", "@swc/core": "^1.15.13", "@types/bcryptjs": "^2.4.6", "@types/express": "^5.0.0", - "@types/jest": "^29.5.12", + "@types/express-serve-static-core": "^5.0.0", "@types/node": "^22.7.5", "@types/passport-jwt": "^4.0.1", "@types/passport-local": "^1.0.38", "@types/pdf-parse": "^1.1.5", "@types/pg": "^8.15.6", "@vitest/ui": "^4.0.16", - "jest": "^29.7.0", "nodemon": "^3.1.7", "prisma": "^7.1.0", "supertest": "^6.3.4", - "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "tslib": "^2.8.0", diff --git a/apps/server/scripts/post-build.sh b/apps/api/scripts/post-build.sh similarity index 100% rename from apps/server/scripts/post-build.sh rename to apps/api/scripts/post-build.sh diff --git a/apps/server/src/config/config.module.ts b/apps/api/src/config/config.module.ts similarity index 100% rename from apps/server/src/config/config.module.ts rename to apps/api/src/config/config.module.ts diff --git a/apps/server/src/config/cors.config.ts b/apps/api/src/config/cors.config.ts similarity index 100% rename from apps/server/src/config/cors.config.ts rename to apps/api/src/config/cors.config.ts diff --git a/apps/server/src/config/cors.configuration.ts b/apps/api/src/config/cors.configuration.ts similarity index 91% rename from apps/server/src/config/cors.configuration.ts rename to apps/api/src/config/cors.configuration.ts index ef4de6d..740cf89 100644 --- a/apps/server/src/config/cors.configuration.ts +++ b/apps/api/src/config/cors.configuration.ts @@ -7,7 +7,7 @@ export const configureCors = (app: INestApplication): void => { const isDevelopment = corsConfig.isDevelopment(); app.enableCors({ - origin: (origin, callback) => { + origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => { if (!origin) { return callback(null, true); } diff --git a/apps/server/src/config/env.validation.ts b/apps/api/src/config/env.validation.ts similarity index 100% rename from apps/server/src/config/env.validation.ts rename to apps/api/src/config/env.validation.ts diff --git a/apps/server/src/config/exception-to-http.pipe.ts b/apps/api/src/config/exception-to-http.pipe.ts similarity index 98% rename from apps/server/src/config/exception-to-http.pipe.ts rename to apps/api/src/config/exception-to-http.pipe.ts index 1cc8360..af709ef 100644 --- a/apps/server/src/config/exception-to-http.pipe.ts +++ b/apps/api/src/config/exception-to-http.pipe.ts @@ -2,7 +2,7 @@ import { AuthenticationError, AuthorizationError, EntityNotFoundError, -} from "@cv/auth"; +} from "@cv/core"; import { ArgumentsHost, Catch, diff --git a/apps/server/src/config/file-storage.config.ts b/apps/api/src/config/file-storage.config.ts similarity index 100% rename from apps/server/src/config/file-storage.config.ts rename to apps/api/src/config/file-storage.config.ts diff --git a/apps/server/src/config/jwt.config.ts b/apps/api/src/config/jwt.config.ts similarity index 100% rename from apps/server/src/config/jwt.config.ts rename to apps/api/src/config/jwt.config.ts diff --git a/apps/server/src/domain/base.entity.ts b/apps/api/src/domain/base.entity.ts similarity index 100% rename from apps/server/src/domain/base.entity.ts rename to apps/api/src/domain/base.entity.ts diff --git a/apps/server/src/domain/errors/app-error.ts b/apps/api/src/domain/errors/app-error.ts similarity index 100% rename from apps/server/src/domain/errors/app-error.ts rename to apps/api/src/domain/errors/app-error.ts diff --git a/apps/server/src/domain/errors/authentication.error.ts b/apps/api/src/domain/errors/authentication.error.ts similarity index 100% rename from apps/server/src/domain/errors/authentication.error.ts rename to apps/api/src/domain/errors/authentication.error.ts diff --git a/apps/server/src/domain/errors/authorization.error.ts b/apps/api/src/domain/errors/authorization.error.ts similarity index 100% rename from apps/server/src/domain/errors/authorization.error.ts rename to apps/api/src/domain/errors/authorization.error.ts diff --git a/apps/server/src/domain/errors/conflict.error.ts b/apps/api/src/domain/errors/conflict.error.ts similarity index 100% rename from apps/server/src/domain/errors/conflict.error.ts rename to apps/api/src/domain/errors/conflict.error.ts diff --git a/apps/server/src/domain/errors/error-codes.ts b/apps/api/src/domain/errors/error-codes.ts similarity index 100% rename from apps/server/src/domain/errors/error-codes.ts rename to apps/api/src/domain/errors/error-codes.ts diff --git a/apps/server/src/domain/errors/not-found.error.ts b/apps/api/src/domain/errors/not-found.error.ts similarity index 100% rename from apps/server/src/domain/errors/not-found.error.ts rename to apps/api/src/domain/errors/not-found.error.ts diff --git a/apps/server/src/main.ts b/apps/api/src/main.ts similarity index 100% rename from apps/server/src/main.ts rename to apps/api/src/main.ts diff --git a/apps/server/src/modules/admin/admin.module.ts b/apps/api/src/modules/admin/admin.module.ts similarity index 97% rename from apps/server/src/modules/admin/admin.module.ts rename to apps/api/src/modules/admin/admin.module.ts index 7949696..5eef4b6 100644 --- a/apps/server/src/modules/admin/admin.module.ts +++ b/apps/api/src/modules/admin/admin.module.ts @@ -1,5 +1,5 @@ import { AIModule } from "@cv/ai-provider"; -import { BaseModule } from "@cv/system"; +import { BaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { ApplicationStatusModule } from "@/modules/application/application-status/application-status.module"; import { EducationModule } from "@/modules/education/education.module"; diff --git a/apps/server/src/modules/admin/admin.resolver.ts b/apps/api/src/modules/admin/admin.resolver.ts similarity index 99% rename from apps/server/src/modules/admin/admin.resolver.ts rename to apps/api/src/modules/admin/admin.resolver.ts index 592a22e..939d70a 100644 --- a/apps/server/src/modules/admin/admin.resolver.ts +++ b/apps/api/src/modules/admin/admin.resolver.ts @@ -5,7 +5,7 @@ import { } from "@cv/ai-provider"; import { AdminGuard, JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { Inject, Optional, UseGuards } from "@nestjs/common"; -import { PageInfo } from "@cv/system"; +import { PageInfo } from "@cv/core"; import { Args, Int, Query, Resolver } from "@nestjs/graphql"; import { AiCallLogEntryType, diff --git a/apps/server/src/modules/admin/admin.type.ts b/apps/api/src/modules/admin/admin.type.ts similarity index 100% rename from apps/server/src/modules/admin/admin.type.ts rename to apps/api/src/modules/admin/admin.type.ts diff --git a/apps/server/src/modules/admin/ai-call-log-persistence.service.ts b/apps/api/src/modules/admin/ai-call-log-persistence.service.ts similarity index 100% rename from apps/server/src/modules/admin/ai-call-log-persistence.service.ts rename to apps/api/src/modules/admin/ai-call-log-persistence.service.ts diff --git a/apps/server/src/modules/admin/ai-call-log.module.ts b/apps/api/src/modules/admin/ai-call-log.module.ts similarity index 100% rename from apps/server/src/modules/admin/ai-call-log.module.ts rename to apps/api/src/modules/admin/ai-call-log.module.ts diff --git a/apps/server/src/modules/admin/ai-call-log.service.ts b/apps/api/src/modules/admin/ai-call-log.service.ts similarity index 100% rename from apps/server/src/modules/admin/ai-call-log.service.ts rename to apps/api/src/modules/admin/ai-call-log.service.ts diff --git a/apps/server/src/modules/admin/graphql/admin-lookup.resolver.ts b/apps/api/src/modules/admin/graphql/admin-lookup.resolver.ts similarity index 98% rename from apps/server/src/modules/admin/graphql/admin-lookup.resolver.ts rename to apps/api/src/modules/admin/graphql/admin-lookup.resolver.ts index c1b435d..726d19b 100644 --- a/apps/server/src/modules/admin/graphql/admin-lookup.resolver.ts +++ b/apps/api/src/modules/admin/graphql/admin-lookup.resolver.ts @@ -1,6 +1,6 @@ import { AdminGuard, JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; -import type { NamedEntity } from "@cv/system"; -import { ClockService, UuidFactoryService } from "@cv/system"; +import type { NamedEntity } from "@cv/core"; +import { ClockService, UuidFactoryService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { ApplicationStatusService } from "@/modules/application/application-status/application-status.service"; diff --git a/apps/server/src/modules/admin/graphql/admin-lookup.type.ts b/apps/api/src/modules/admin/graphql/admin-lookup.type.ts similarity index 100% rename from apps/server/src/modules/admin/graphql/admin-lookup.type.ts rename to apps/api/src/modules/admin/graphql/admin-lookup.type.ts diff --git a/apps/server/src/modules/admin/logging-ai-provider.ts b/apps/api/src/modules/admin/logging-ai-provider.ts similarity index 100% rename from apps/server/src/modules/admin/logging-ai-provider.ts rename to apps/api/src/modules/admin/logging-ai-provider.ts diff --git a/apps/server/src/modules/admin/queue-monitor.service.ts b/apps/api/src/modules/admin/queue-monitor.service.ts similarity index 100% rename from apps/server/src/modules/admin/queue-monitor.service.ts rename to apps/api/src/modules/admin/queue-monitor.service.ts diff --git a/apps/server/src/modules/app.module.ts b/apps/api/src/modules/app.module.ts similarity index 96% rename from apps/server/src/modules/app.module.ts rename to apps/api/src/modules/app.module.ts index 8e6f1e2..3731954 100644 --- a/apps/server/src/modules/app.module.ts +++ b/apps/api/src/modules/app.module.ts @@ -1,11 +1,12 @@ -import { AuthModule, AuthorizationModule, UserModule } from "@cv/auth"; +import { AuthorizationModule, EventsModule, UserModule } from "@cv/core"; +import { AuthModule } from "@cv/auth"; import { FileStorageModule } from "@cv/file-storage"; import { BaseModule, DatabaseModule, ResendModule, TemplateModule, -} from "@cv/system"; +} from "@cv/core"; import { ApolloDriver, type ApolloDriverConfig } from "@nestjs/apollo"; import { Module } from "@nestjs/common"; import { ConfigModule, ConfigService } from "@nestjs/config"; @@ -70,6 +71,7 @@ import { VacancyModule } from "./vacancies/vacancy.module"; FileStorageModule.forRootAsync({ useClass: FileStorageConfig, }), + EventsModule, AiCallLogModule, AppConfigModule, BaseModule, diff --git a/apps/server/src/modules/app/app.module.ts b/apps/api/src/modules/app/app.module.ts similarity index 100% rename from apps/server/src/modules/app/app.module.ts rename to apps/api/src/modules/app/app.module.ts diff --git a/apps/server/src/modules/app/app.resolver.ts b/apps/api/src/modules/app/app.resolver.ts similarity index 100% rename from apps/server/src/modules/app/app.resolver.ts rename to apps/api/src/modules/app/app.resolver.ts diff --git a/apps/server/src/modules/app/app.service.ts b/apps/api/src/modules/app/app.service.ts similarity index 100% rename from apps/server/src/modules/app/app.service.ts rename to apps/api/src/modules/app/app.service.ts diff --git a/apps/server/src/modules/app/health.controller.ts b/apps/api/src/modules/app/health.controller.ts similarity index 100% rename from apps/server/src/modules/app/health.controller.ts rename to apps/api/src/modules/app/health.controller.ts diff --git a/apps/server/src/modules/app/health.type.ts b/apps/api/src/modules/app/health.type.ts similarity index 100% rename from apps/server/src/modules/app/health.type.ts rename to apps/api/src/modules/app/health.type.ts diff --git a/apps/server/src/modules/application/application-status.dataloader.ts b/apps/api/src/modules/application/application-status.dataloader.ts similarity index 97% rename from apps/server/src/modules/application/application-status.dataloader.ts rename to apps/api/src/modules/application/application-status.dataloader.ts index 386d5bc..02bc545 100644 --- a/apps/server/src/modules/application/application-status.dataloader.ts +++ b/apps/api/src/modules/application/application-status.dataloader.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { Injectable, Scope } from "@nestjs/common"; import DataLoader from "dataloader"; import type { ApplicationStatusRelation } from "./application.entity"; diff --git a/apps/server/src/modules/application/application-status/application-status.dto.ts b/apps/api/src/modules/application/application-status/application-status.dto.ts similarity index 100% rename from apps/server/src/modules/application/application-status/application-status.dto.ts rename to apps/api/src/modules/application/application-status/application-status.dto.ts diff --git a/apps/server/src/modules/application/application-status/application-status.entity.ts b/apps/api/src/modules/application/application-status/application-status.entity.ts similarity index 56% rename from apps/server/src/modules/application/application-status/application-status.entity.ts rename to apps/api/src/modules/application/application-status/application-status.entity.ts index 438fd13..c5d1d2f 100644 --- a/apps/server/src/modules/application/application-status/application-status.entity.ts +++ b/apps/api/src/modules/application/application-status/application-status.entity.ts @@ -1,3 +1,3 @@ -import { NamedEntity } from "@cv/system"; +import { NamedEntity } from "@cv/core"; export class ApplicationStatus extends NamedEntity {} diff --git a/apps/server/src/modules/application/application-status/application-status.factory.ts b/apps/api/src/modules/application/application-status/application-status.factory.ts similarity index 90% rename from apps/server/src/modules/application/application-status/application-status.factory.ts rename to apps/api/src/modules/application/application-status/application-status.factory.ts index dd46c2f..312f7e4 100644 --- a/apps/server/src/modules/application/application-status/application-status.factory.ts +++ b/apps/api/src/modules/application/application-status/application-status.factory.ts @@ -1,4 +1,4 @@ -import { ClockService, Factory, UuidFactoryService } from "@cv/system"; +import { ClockService, Factory, UuidFactoryService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { CreateApplicationStatusDto } from "./application-status.dto"; import { ApplicationStatus } from "./application-status.entity"; diff --git a/apps/server/src/modules/application/application-status/application-status.mapper.ts b/apps/api/src/modules/application/application-status/application-status.mapper.ts similarity index 94% rename from apps/server/src/modules/application/application-status/application-status.mapper.ts rename to apps/api/src/modules/application/application-status/application-status.mapper.ts index 4f16071..5e37c01 100644 --- a/apps/server/src/modules/application/application-status/application-status.mapper.ts +++ b/apps/api/src/modules/application/application-status/application-status.mapper.ts @@ -1,4 +1,4 @@ -import { createNamedEntityMapper } from "@cv/system"; +import { createNamedEntityMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { ApplicationStatus as PrismaApplicationStatus } from "@prisma/client"; import { ApplicationStatus } from "./application-status.entity"; diff --git a/apps/server/src/modules/application/application-status/application-status.module.ts b/apps/api/src/modules/application/application-status/application-status.module.ts similarity index 90% rename from apps/server/src/modules/application/application-status/application-status.module.ts rename to apps/api/src/modules/application/application-status/application-status.module.ts index 57c336b..c7e2f1d 100644 --- a/apps/server/src/modules/application/application-status/application-status.module.ts +++ b/apps/api/src/modules/application/application-status/application-status.module.ts @@ -1,4 +1,4 @@ -import { BaseModule, DatabaseModule } from "@cv/system"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { ApplicationStatusFactory } from "./application-status.factory"; import { ApplicationStatusMapper } from "./application-status.mapper"; diff --git a/apps/server/src/modules/application/application-status/application-status.service.ts b/apps/api/src/modules/application/application-status/application-status.service.ts similarity index 88% rename from apps/server/src/modules/application/application-status/application-status.service.ts rename to apps/api/src/modules/application/application-status/application-status.service.ts index 23495ab..b11a5d7 100644 --- a/apps/server/src/modules/application/application-status/application-status.service.ts +++ b/apps/api/src/modules/application/application-status/application-status.service.ts @@ -1,4 +1,4 @@ -import { NamedEntityService, PrismaService } from "@cv/system"; +import { NamedEntityService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { ApplicationStatus } from "./application-status.entity"; import { ApplicationStatusMapper } from "./application-status.mapper"; diff --git a/apps/server/src/modules/application/application.entity.ts b/apps/api/src/modules/application/application.entity.ts similarity index 96% rename from apps/server/src/modules/application/application.entity.ts rename to apps/api/src/modules/application/application.entity.ts index 3cbc95e..c1614af 100644 --- a/apps/server/src/modules/application/application.entity.ts +++ b/apps/api/src/modules/application/application.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; import { CV } from "@/modules/cv-template/graphql/cv.type"; import { Vacancy } from "@/modules/vacancies/vacancy.entity"; diff --git a/apps/server/src/modules/application/application.error.ts b/apps/api/src/modules/application/application.error.ts similarity index 79% rename from apps/server/src/modules/application/application.error.ts rename to apps/api/src/modules/application/application.error.ts index bef4c66..c8de26f 100644 --- a/apps/server/src/modules/application/application.error.ts +++ b/apps/api/src/modules/application/application.error.ts @@ -1,4 +1,4 @@ -import { DomainError } from "@cv/system"; +import { DomainError } from "@cv/core"; export class DuplicateApplicationError extends DomainError { constructor() { diff --git a/apps/server/src/modules/application/application.mapper.ts b/apps/api/src/modules/application/application.mapper.ts similarity index 100% rename from apps/server/src/modules/application/application.mapper.ts rename to apps/api/src/modules/application/application.mapper.ts diff --git a/apps/server/src/modules/application/application.module.ts b/apps/api/src/modules/application/application.module.ts similarity index 89% rename from apps/server/src/modules/application/application.module.ts rename to apps/api/src/modules/application/application.module.ts index d612041..581a533 100644 --- a/apps/server/src/modules/application/application.module.ts +++ b/apps/api/src/modules/application/application.module.ts @@ -1,5 +1,6 @@ -import { AuthModule, AuthorizationModule } from "@cv/auth"; -import { DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { AuthModule } from "@cv/auth"; +import { DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { CVTemplateModule } from "@/modules/cv-template/cv-template.module"; diff --git a/apps/server/src/modules/application/application.policy.ts b/apps/api/src/modules/application/application.policy.ts similarity index 77% rename from apps/server/src/modules/application/application.policy.ts rename to apps/api/src/modules/application/application.policy.ts index 278e667..8501d92 100644 --- a/apps/server/src/modules/application/application.policy.ts +++ b/apps/api/src/modules/application/application.policy.ts @@ -1,4 +1,4 @@ -import { Policy, UserOwnedResourcePolicy } from "@cv/auth"; +import { Policy, UserOwnedResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Application } from "./application.entity"; diff --git a/apps/server/src/modules/application/application.service.ts b/apps/api/src/modules/application/application.service.ts similarity index 97% rename from apps/server/src/modules/application/application.service.ts rename to apps/api/src/modules/application/application.service.ts index 20f57f2..3cd4ada 100644 --- a/apps/server/src/modules/application/application.service.ts +++ b/apps/api/src/modules/application/application.service.ts @@ -1,5 +1,5 @@ -import { notFound } from "@cv/auth"; -import { type EntityService, PrismaService } from "@cv/system"; +import { notFound } from "@cv/core"; +import { type EntityService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { Application } from "./application.entity"; diff --git a/apps/server/src/modules/application/graphql/application.input.ts b/apps/api/src/modules/application/graphql/application.input.ts similarity index 100% rename from apps/server/src/modules/application/graphql/application.input.ts rename to apps/api/src/modules/application/graphql/application.input.ts diff --git a/apps/server/src/modules/application/graphql/application.resolver.ts b/apps/api/src/modules/application/graphql/application.resolver.ts similarity index 96% rename from apps/server/src/modules/application/graphql/application.resolver.ts rename to apps/api/src/modules/application/graphql/application.resolver.ts index 3f19454..e73fa9a 100644 --- a/apps/server/src/modules/application/graphql/application.resolver.ts +++ b/apps/api/src/modules/application/graphql/application.resolver.ts @@ -1,10 +1,7 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; +import { PrismaService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, diff --git a/apps/server/src/modules/application/graphql/application.type.ts b/apps/api/src/modules/application/graphql/application.type.ts similarity index 100% rename from apps/server/src/modules/application/graphql/application.type.ts rename to apps/api/src/modules/application/graphql/application.type.ts diff --git a/apps/server/src/modules/application/graphql/user-field.resolver.ts b/apps/api/src/modules/application/graphql/user-field.resolver.ts similarity index 96% rename from apps/server/src/modules/application/graphql/user-field.resolver.ts rename to apps/api/src/modules/application/graphql/user-field.resolver.ts index 3226726..8bb9c0c 100644 --- a/apps/server/src/modules/application/graphql/user-field.resolver.ts +++ b/apps/api/src/modules/application/graphql/user-field.resolver.ts @@ -1,5 +1,5 @@ import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; -import { PageInfo } from "@cv/system"; +import { PageInfo } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Parent, ResolveField, Resolver } from "@nestjs/graphql"; import { User } from "@/modules/user/user.type"; diff --git a/apps/server/src/modules/authentication/authentication.module.ts b/apps/api/src/modules/authentication/authentication.module.ts similarity index 87% rename from apps/server/src/modules/authentication/authentication.module.ts rename to apps/api/src/modules/authentication/authentication.module.ts index a76a020..f86c4bb 100644 --- a/apps/server/src/modules/authentication/authentication.module.ts +++ b/apps/api/src/modules/authentication/authentication.module.ts @@ -4,10 +4,9 @@ import { DatabaseModule, ResendModule, TemplateModule, -} from "@cv/system"; +} from "@cv/core"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; -import { EventEmitterModule } from "@nestjs/event-emitter"; import { AuthenticationService } from "./authentication.service"; import { PasswordProviderModule } from "./providers/password/password-provider.module"; import { AuthenticationController } from "./rest/authentication.controller"; @@ -17,7 +16,6 @@ import { TokenResolver } from "./token/token.resolver"; imports: [ ConfigModule, AuthModule, - EventEmitterModule.forRoot(), BaseModule, DatabaseModule, ResendModule, diff --git a/apps/server/src/modules/authentication/authentication.service.ts b/apps/api/src/modules/authentication/authentication.service.ts similarity index 85% rename from apps/server/src/modules/authentication/authentication.service.ts rename to apps/api/src/modules/authentication/authentication.service.ts index 6fa452a..09cb270 100644 --- a/apps/server/src/modules/authentication/authentication.service.ts +++ b/apps/api/src/modules/authentication/authentication.service.ts @@ -1,5 +1,5 @@ -import type { User } from "@cv/auth"; -import { UserService } from "@cv/auth"; +import type { User } from "@cv/core"; +import { UserService } from "@cv/core"; import { Injectable } from "@nestjs/common"; export interface RefreshTokenDto { refresh_token: string; @@ -14,8 +14,8 @@ type InternalRefreshTokenResponse = RefreshTokenResponse & { refresh_token: string; }; -import type { RequestMetadata } from "@cv/auth"; -import { TokenService } from "@cv/auth"; +import type { RequestMetadata } from "@cv/core"; +import { TokenService } from "@cv/core"; @Injectable() export class AuthenticationService { diff --git a/apps/server/src/modules/authentication/graphql/authentication.type.ts b/apps/api/src/modules/authentication/graphql/authentication.type.ts similarity index 100% rename from apps/server/src/modules/authentication/graphql/authentication.type.ts rename to apps/api/src/modules/authentication/graphql/authentication.type.ts diff --git a/apps/server/src/modules/authentication/graphql/graphql-context.type.ts b/apps/api/src/modules/authentication/graphql/graphql-context.type.ts similarity index 100% rename from apps/server/src/modules/authentication/graphql/graphql-context.type.ts rename to apps/api/src/modules/authentication/graphql/graphql-context.type.ts diff --git a/apps/server/src/modules/authentication/providers/password/graphql/password-authentication.resolver.ts b/apps/api/src/modules/authentication/providers/password/graphql/password-authentication.resolver.ts similarity index 95% rename from apps/server/src/modules/authentication/providers/password/graphql/password-authentication.resolver.ts rename to apps/api/src/modules/authentication/providers/password/graphql/password-authentication.resolver.ts index e92a57e..b87889c 100644 --- a/apps/server/src/modules/authentication/providers/password/graphql/password-authentication.resolver.ts +++ b/apps/api/src/modules/authentication/providers/password/graphql/password-authentication.resolver.ts @@ -1,12 +1,6 @@ -import { - AuthCookieService, - AuthorizationService, - type User as DomainUser, - JwtAuthGuard, - PasswordAuthenticationService, - RequestMetadata, - VerifiedScopeGuard, -} from "@cv/auth"; +import { AuthCookieService, AuthorizationService, type User as DomainUser, PasswordAuthenticationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; +import { RequestMetadata } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Context, Mutation, Resolver } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; diff --git a/apps/server/src/modules/authentication/providers/password/password-provider.module.ts b/apps/api/src/modules/authentication/providers/password/password-provider.module.ts similarity index 88% rename from apps/server/src/modules/authentication/providers/password/password-provider.module.ts rename to apps/api/src/modules/authentication/providers/password/password-provider.module.ts index 26d295b..95462e3 100644 --- a/apps/server/src/modules/authentication/providers/password/password-provider.module.ts +++ b/apps/api/src/modules/authentication/providers/password/password-provider.module.ts @@ -6,17 +6,15 @@ import { RegistrationAttemptEmailListener, TemplatedEmailService, VerificationEmailListener, -} from "@cv/auth"; -import { ResendModule, TemplateModule } from "@cv/system"; +} from "@cv/core"; +import { ResendModule, TemplateModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { ConfigModule, ConfigService } from "@nestjs/config"; -import { EventEmitterModule } from "@nestjs/event-emitter"; import { PasswordAuthenticationResolver } from "./graphql/password-authentication.resolver"; @Module({ imports: [ ConfigModule, - EventEmitterModule.forRoot(), AuthPasswordProviderModule, ResendModule, TemplateModule, diff --git a/apps/server/src/modules/authentication/request/request-with-cookies.type.ts b/apps/api/src/modules/authentication/request/request-with-cookies.type.ts similarity index 100% rename from apps/server/src/modules/authentication/request/request-with-cookies.type.ts rename to apps/api/src/modules/authentication/request/request-with-cookies.type.ts diff --git a/apps/server/src/modules/authentication/rest/authentication.controller.ts b/apps/api/src/modules/authentication/rest/authentication.controller.ts similarity index 93% rename from apps/server/src/modules/authentication/rest/authentication.controller.ts rename to apps/api/src/modules/authentication/rest/authentication.controller.ts index 7cfb483..e5dd6b1 100644 --- a/apps/server/src/modules/authentication/rest/authentication.controller.ts +++ b/apps/api/src/modules/authentication/rest/authentication.controller.ts @@ -1,4 +1,5 @@ -import { AuthCookieService, RequestMetadata } from "@cv/auth"; +import { AuthCookieService } from "@cv/core"; +import { RequestMetadata } from "@cv/core"; import { Controller, Post, Res } from "@nestjs/common"; import type { Response } from "express"; import { AuthenticationService } from "../authentication.service"; diff --git a/apps/server/src/modules/authentication/token/active-session.type.ts b/apps/api/src/modules/authentication/token/active-session.type.ts similarity index 100% rename from apps/server/src/modules/authentication/token/active-session.type.ts rename to apps/api/src/modules/authentication/token/active-session.type.ts diff --git a/apps/server/src/modules/authentication/token/refresh-token-cookie.decorator.ts b/apps/api/src/modules/authentication/token/refresh-token-cookie.decorator.ts similarity index 94% rename from apps/server/src/modules/authentication/token/refresh-token-cookie.decorator.ts rename to apps/api/src/modules/authentication/token/refresh-token-cookie.decorator.ts index 0b5a126..09876bc 100644 --- a/apps/server/src/modules/authentication/token/refresh-token-cookie.decorator.ts +++ b/apps/api/src/modules/authentication/token/refresh-token-cookie.decorator.ts @@ -1,4 +1,4 @@ -import { createGqlParamDecorator, unauthorized } from "@cv/system"; +import { createGqlParamDecorator, unauthorized } from "@cv/core"; import type { ExecutionContext } from "@nestjs/common"; import { GqlExecutionContext } from "@nestjs/graphql"; import { z } from "zod/v4"; diff --git a/apps/server/src/modules/authentication/token/refresh-token-response.dto.ts b/apps/api/src/modules/authentication/token/refresh-token-response.dto.ts similarity index 100% rename from apps/server/src/modules/authentication/token/refresh-token-response.dto.ts rename to apps/api/src/modules/authentication/token/refresh-token-response.dto.ts diff --git a/apps/server/src/modules/authentication/token/token-expiration.type.ts b/apps/api/src/modules/authentication/token/token-expiration.type.ts similarity index 100% rename from apps/server/src/modules/authentication/token/token-expiration.type.ts rename to apps/api/src/modules/authentication/token/token-expiration.type.ts diff --git a/apps/server/src/modules/authentication/token/token.module.ts b/apps/api/src/modules/authentication/token/token.module.ts similarity index 70% rename from apps/server/src/modules/authentication/token/token.module.ts rename to apps/api/src/modules/authentication/token/token.module.ts index 51dbd2f..ebe8d6d 100644 --- a/apps/server/src/modules/authentication/token/token.module.ts +++ b/apps/api/src/modules/authentication/token/token.module.ts @@ -1,4 +1,4 @@ -import { TokenModule as AuthTokenModule } from "@cv/auth"; +import { TokenModule as AuthTokenModule } from "@cv/core"; import { Module } from "@nestjs/common"; @Module({ diff --git a/apps/server/src/modules/authentication/token/token.resolver.ts b/apps/api/src/modules/authentication/token/token.resolver.ts similarity index 93% rename from apps/server/src/modules/authentication/token/token.resolver.ts rename to apps/api/src/modules/authentication/token/token.resolver.ts index c4f4fa3..d560436 100644 --- a/apps/server/src/modules/authentication/token/token.resolver.ts +++ b/apps/api/src/modules/authentication/token/token.resolver.ts @@ -1,12 +1,5 @@ -import { - AuthCookieService, - AuthorizationService, - type User as DomainUser, - JwtAuthGuard, - RefreshToken, - RefreshTokenService, - VerifiedScopeGuard, -} from "@cv/auth"; +import { AuthCookieService, AuthorizationService, type User as DomainUser, RefreshToken, RefreshTokenService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Args, Context, Mutation, Query, Resolver } from "@nestjs/graphql"; import type { Response } from "express"; diff --git a/apps/api/src/modules/base/base-graphql-type.ts b/apps/api/src/modules/base/base-graphql-type.ts new file mode 100644 index 0000000..6a39e2a --- /dev/null +++ b/apps/api/src/modules/base/base-graphql-type.ts @@ -0,0 +1,19 @@ +import { Field, ID, ObjectType } from "@nestjs/graphql"; + +@ObjectType({ isAbstract: true }) +export abstract class BaseGraphQLType { + @Field(() => ID) + id: string; + + @Field() + createdAt: Date; + + @Field() + updatedAt: Date; + + constructor(id: string, createdAt: Date, updatedAt: Date) { + this.id = id; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } +} diff --git a/apps/server/src/modules/base/base.entity.ts b/apps/api/src/modules/base/base.entity.ts similarity index 100% rename from apps/server/src/modules/base/base.entity.ts rename to apps/api/src/modules/base/base.entity.ts diff --git a/apps/server/src/modules/base/base.module.ts b/apps/api/src/modules/base/base.module.ts similarity index 88% rename from apps/server/src/modules/base/base.module.ts rename to apps/api/src/modules/base/base.module.ts index f4faf8b..834a391 100644 --- a/apps/server/src/modules/base/base.module.ts +++ b/apps/api/src/modules/base/base.module.ts @@ -1,4 +1,4 @@ -import { ClockService, UuidFactoryService } from "@cv/system"; +import { ClockService, UuidFactoryService } from "@cv/core"; import { Module } from "@nestjs/common"; import { CursorService } from "./cursor.service"; import { ErrorCode } from "./error-code.enum"; diff --git a/apps/server/src/modules/base/connection.factory.ts b/apps/api/src/modules/base/connection.factory.ts similarity index 95% rename from apps/server/src/modules/base/connection.factory.ts rename to apps/api/src/modules/base/connection.factory.ts index 6ff638f..79a101c 100644 --- a/apps/server/src/modules/base/connection.factory.ts +++ b/apps/api/src/modules/base/connection.factory.ts @@ -1,4 +1,4 @@ -import { PageInfo, type PaginationResult } from "@cv/system"; +import { PageInfo, type PaginationResult } from "@cv/core"; import { raise } from "@cv/utils"; import type { Type } from "@nestjs/common"; import { Field, Int, ObjectType } from "@nestjs/graphql"; diff --git a/apps/server/src/modules/base/connection.types.ts b/apps/api/src/modules/base/connection.types.ts similarity index 100% rename from apps/server/src/modules/base/connection.types.ts rename to apps/api/src/modules/base/connection.types.ts diff --git a/apps/server/src/modules/base/cursor.service.ts b/apps/api/src/modules/base/cursor.service.ts similarity index 100% rename from apps/server/src/modules/base/cursor.service.ts rename to apps/api/src/modules/base/cursor.service.ts diff --git a/apps/server/src/modules/base/error-code.enum.ts b/apps/api/src/modules/base/error-code.enum.ts similarity index 100% rename from apps/server/src/modules/base/error-code.enum.ts rename to apps/api/src/modules/base/error-code.enum.ts diff --git a/apps/server/src/modules/base/event.interface.ts b/apps/api/src/modules/base/event.interface.ts similarity index 100% rename from apps/server/src/modules/base/event.interface.ts rename to apps/api/src/modules/base/event.interface.ts diff --git a/apps/server/src/modules/base/graphql-exception.filter.ts b/apps/api/src/modules/base/graphql-exception.filter.ts similarity index 98% rename from apps/server/src/modules/base/graphql-exception.filter.ts rename to apps/api/src/modules/base/graphql-exception.filter.ts index eb2d1aa..89c8146 100644 --- a/apps/server/src/modules/base/graphql-exception.filter.ts +++ b/apps/api/src/modules/base/graphql-exception.filter.ts @@ -2,8 +2,8 @@ import { AuthenticationError, AuthorizationError, EntityNotFoundError, -} from "@cv/auth"; -import { DomainError } from "@cv/system"; +} from "@cv/core"; +import { DomainError } from "@cv/core"; import { ArgumentsHost, Catch, diff --git a/apps/server/src/modules/base/mapper.interface.ts b/apps/api/src/modules/base/mapper.interface.ts similarity index 100% rename from apps/server/src/modules/base/mapper.interface.ts rename to apps/api/src/modules/base/mapper.interface.ts diff --git a/apps/server/src/modules/base/named-graphql-type.factory.ts b/apps/api/src/modules/base/named-graphql-type.factory.ts similarity index 82% rename from apps/server/src/modules/base/named-graphql-type.factory.ts rename to apps/api/src/modules/base/named-graphql-type.factory.ts index 767b27c..782fe5d 100644 --- a/apps/server/src/modules/base/named-graphql-type.factory.ts +++ b/apps/api/src/modules/base/named-graphql-type.factory.ts @@ -1,6 +1,7 @@ -import type { NamedEntity } from "@cv/system"; +import type { NamedEntity } from "@cv/core"; import type { Type } from "@nestjs/common"; -import { Field, ID, ObjectType } from "@nestjs/graphql"; +import { Field, ObjectType } from "@nestjs/graphql"; +import { BaseGraphQLType } from "./base-graphql-type"; import { createConnection } from "./connection.factory"; /** @@ -17,22 +18,13 @@ export function createNamedGraphQLType( _DomainClass: abstract new (...args: any[]) => TDomain, ) { @ObjectType(name) - class NamedGraphQLType { - @Field(() => ID) - id: string; - + class NamedGraphQLType extends BaseGraphQLType { @Field() name: string; @Field({ nullable: true }) description?: string; - @Field() - createdAt: Date; - - @Field() - updatedAt: Date; - constructor( id: string, name: string, @@ -40,10 +32,8 @@ export function createNamedGraphQLType( updatedAt: Date, description?: string, ) { - this.id = id; + super(id, createdAt, updatedAt); this.name = name; - this.createdAt = createdAt; - this.updatedAt = updatedAt; if (description !== undefined) { this.description = description; } diff --git a/apps/server/src/modules/base/not-found.util.ts b/apps/api/src/modules/base/not-found.util.ts similarity index 90% rename from apps/server/src/modules/base/not-found.util.ts rename to apps/api/src/modules/base/not-found.util.ts index 2a56223..342a735 100644 --- a/apps/server/src/modules/base/not-found.util.ts +++ b/apps/api/src/modules/base/not-found.util.ts @@ -1,4 +1,4 @@ -import { EntityNotFoundError } from "@cv/auth"; +import { EntityNotFoundError } from "@cv/core"; /** * Utility function to throw an EntityNotFoundError with a consistent message diff --git a/apps/server/src/modules/base/pagination.service.ts b/apps/api/src/modules/base/pagination.service.ts similarity index 100% rename from apps/server/src/modules/base/pagination.service.ts rename to apps/api/src/modules/base/pagination.service.ts diff --git a/apps/server/src/modules/base/pagination.types.ts b/apps/api/src/modules/base/pagination.types.ts similarity index 100% rename from apps/server/src/modules/base/pagination.types.ts rename to apps/api/src/modules/base/pagination.types.ts diff --git a/apps/server/src/modules/base/raise.util.ts b/apps/api/src/modules/base/raise.util.ts similarity index 100% rename from apps/server/src/modules/base/raise.util.ts rename to apps/api/src/modules/base/raise.util.ts diff --git a/apps/server/src/modules/base/unauthorized.util.ts b/apps/api/src/modules/base/unauthorized.util.ts similarity index 100% rename from apps/server/src/modules/base/unauthorized.util.ts rename to apps/api/src/modules/base/unauthorized.util.ts diff --git a/apps/server/src/modules/current-user/current-refresh-token-id.decorator.ts b/apps/api/src/modules/current-user/current-refresh-token-id.decorator.ts similarity index 85% rename from apps/server/src/modules/current-user/current-refresh-token-id.decorator.ts rename to apps/api/src/modules/current-user/current-refresh-token-id.decorator.ts index e0aa65e..d30ae29 100644 --- a/apps/server/src/modules/current-user/current-refresh-token-id.decorator.ts +++ b/apps/api/src/modules/current-user/current-refresh-token-id.decorator.ts @@ -1,4 +1,4 @@ -import { createGqlParamDecorator } from "@cv/system"; +import { createGqlParamDecorator } from "@cv/core"; import { z } from "zod/v4"; const jwtRequestSchema = z.looseObject({ diff --git a/apps/server/src/modules/current-user/current-user.decorator.ts b/apps/api/src/modules/current-user/current-user.decorator.ts similarity index 84% rename from apps/server/src/modules/current-user/current-user.decorator.ts rename to apps/api/src/modules/current-user/current-user.decorator.ts index 743af1b..3584612 100644 --- a/apps/server/src/modules/current-user/current-user.decorator.ts +++ b/apps/api/src/modules/current-user/current-user.decorator.ts @@ -1,4 +1,4 @@ -import { createGqlParamDecorator, raise } from "@cv/system"; +import { createGqlParamDecorator, raise } from "@cv/core"; import { z } from "zod/v4"; const requestSchema = z.looseObject({ diff --git a/apps/server/src/modules/current-user/current-user.module.ts b/apps/api/src/modules/current-user/current-user.module.ts similarity index 89% rename from apps/server/src/modules/current-user/current-user.module.ts rename to apps/api/src/modules/current-user/current-user.module.ts index 06f0fb2..a83586b 100644 --- a/apps/server/src/modules/current-user/current-user.module.ts +++ b/apps/api/src/modules/current-user/current-user.module.ts @@ -1,4 +1,4 @@ -import { UserModule } from "@cv/auth"; +import { UserModule } from "@cv/core"; import { forwardRef, Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { MeResolver } from "./me.resolver"; diff --git a/apps/server/src/modules/current-user/index.ts b/apps/api/src/modules/current-user/index.ts similarity index 100% rename from apps/server/src/modules/current-user/index.ts rename to apps/api/src/modules/current-user/index.ts diff --git a/apps/server/src/modules/current-user/me.resolver.ts b/apps/api/src/modules/current-user/me.resolver.ts similarity index 78% rename from apps/server/src/modules/current-user/me.resolver.ts rename to apps/api/src/modules/current-user/me.resolver.ts index 750112b..bbc4c8e 100644 --- a/apps/server/src/modules/current-user/me.resolver.ts +++ b/apps/api/src/modules/current-user/me.resolver.ts @@ -1,5 +1,6 @@ -import type { User as DomainUser } from "@cv/auth"; -import { JwtAuthGuard, UserService, VerifiedScopeGuard } from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; +import { UserService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Query, Resolver } from "@nestjs/graphql"; import { User } from "@/modules/user/user.type"; diff --git a/apps/api/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts b/apps/api/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts new file mode 100644 index 0000000..0779251 --- /dev/null +++ b/apps/api/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts @@ -0,0 +1,348 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { User } from "@cv/core"; +import type { Mocked } from "vitest"; +import type { AIProviderResolverService } from "../ai-provider-resolver.service"; +import { CVParserService } from "../cv-parser.service"; +import type { EntityResolverService } from "../entity-resolver.service"; +import { + mockJaneDoeParsedCV, + mockJohnSmithParsedCV, +} from "./fixtures/mock-ai-responses"; + +const mockParseCVText = vi.fn(); + +vi.mock("@cv/ai-parser", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + CVParserService: class MockCVParser { + parseCVText = mockParseCVText; + }, + }; +}); + +describe("CVParserService", () => { + let service: CVParserService; + let mockTextExtractorRegistry: { + extract: Mock; + }; + let mockEntityResolver: Mocked; + + const mockUser = new User( + "user-1", + "Test User", + new Date(), + new Date(), + null, + ); + + const fixturesPath = join(__dirname, "fixtures"); + const sampleTxtCV = readFileSync( + join(fixturesPath, "sample-cv.txt"), + "utf-8", + ); + const sampleMdCV = readFileSync(join(fixturesPath, "sample-cv.md"), "utf-8"); + + beforeEach(() => { + mockParseCVText.mockReset(); + + mockTextExtractorRegistry = { + extract: vi.fn(), + }; + + mockEntityResolver = { + resolveCompany: vi.fn(), + resolveRole: vi.fn(), + resolveLevel: vi.fn(), + resolveSkill: vi.fn(), + resolveSkills: vi.fn(), + resolveInstitution: vi.fn(), + } as unknown as Mocked; + + const mockProviderResolver = { + resolveForUser: vi.fn().mockResolvedValue({ + generateText: vi.fn(), + }), + } as unknown as AIProviderResolverService; + + service = new CVParserService( + mockTextExtractorRegistry as any, + mockEntityResolver, + mockProviderResolver, + ); + }); + + describe("parseStory", () => { + it("parses a career narrative from plain text", async () => { + mockParseCVText.mockResolvedValue(mockJohnSmithParsedCV); + + const result = await service.parseStory(mockUser, sampleTxtCV); + + expect(mockParseCVText).toHaveBeenCalledWith(sampleTxtCV); + expect(result).toEqual(mockJohnSmithParsedCV); + expect(result.personalInfo?.name).toBe("John Smith"); + expect(result.jobExperiences).toHaveLength(3); + expect(result.education).toHaveLength(1); + }); + + it("parses a career narrative from markdown", async () => { + mockParseCVText.mockResolvedValue(mockJaneDoeParsedCV); + + const result = await service.parseStory(mockUser, sampleMdCV); + + expect(mockParseCVText).toHaveBeenCalledWith(sampleMdCV); + expect(result).toEqual(mockJaneDoeParsedCV); + expect(result.personalInfo?.name).toBe("Jane Doe"); + expect(result.jobExperiences).toHaveLength(3); + expect(result.education).toHaveLength(2); + }); + + it("throws error when story text is empty", async () => { + await expect(service.parseStory(mockUser, "")).rejects.toThrow( + "Story text cannot be empty", + ); + + expect(mockParseCVText).not.toHaveBeenCalled(); + }); + + it("throws error when story text is only whitespace", async () => { + await expect(service.parseStory(mockUser, " \n\t ")).rejects.toThrow( + "Story text cannot be empty", + ); + + expect(mockParseCVText).not.toHaveBeenCalled(); + }); + + it("throws error when story text exceeds maximum length", async () => { + const longText = "a".repeat(50001); + + await expect(service.parseStory(mockUser, longText)).rejects.toThrow( + "Story text is too long (max 50,000 characters)", + ); + + expect(mockParseCVText).not.toHaveBeenCalled(); + }); + + it("accepts story text at maximum length boundary", async () => { + const maxLengthText = "a".repeat(50000); + mockParseCVText.mockResolvedValue({ + jobExperiences: [], + education: [], + skills: [], + }); + + await service.parseStory(mockUser, maxLengthText); + + expect(mockParseCVText).toHaveBeenCalledWith(maxLengthText); + }); + + it("extracts job experiences with correct structure", async () => { + mockParseCVText.mockResolvedValue(mockJohnSmithParsedCV); + + const result = await service.parseStory(mockUser, sampleTxtCV); + + const firstJob = result.jobExperiences[0]; + expect(firstJob.companyName).toBe("Acme Corporation"); + expect(firstJob.roleName).toBe("Senior Software Engineer"); + expect(firstJob.levelName).toBe("Senior"); + expect(firstJob.startDate).toBe("2020-01-01"); + expect(firstJob.endDate).toBeNull(); + expect(firstJob.skills).toContain("TypeScript"); + expect(firstJob.skills).toContain("Kubernetes"); + }); + + it("extracts education with correct structure", async () => { + mockParseCVText.mockResolvedValue(mockJohnSmithParsedCV); + + const result = await service.parseStory(mockUser, sampleTxtCV); + + const education = result.education[0]; + expect(education.institutionName).toBe("Stanford University"); + expect(education.degree).toBe("Bachelor of Science"); + expect(education.fieldOfStudy).toBe("Computer Science"); + expect(education.startDate).toBe("2010-09-01"); + expect(education.endDate).toBe("2014-06-30"); + }); + + it("extracts overall skills list", async () => { + mockParseCVText.mockResolvedValue(mockJohnSmithParsedCV); + + const result = await service.parseStory(mockUser, sampleTxtCV); + + expect(result.skills).toContain("TypeScript"); + expect(result.skills).toContain("Docker"); + expect(result.skills).toContain("AWS"); + expect(result.skills.length).toBeGreaterThan(0); + }); + }); + + describe("parseFile", () => { + const mockBuffer = Buffer.from("mock file content"); + const mockMimeType = "text/plain"; + const mockFileName = "resume.txt"; + + it("parses a valid text file", async () => { + mockTextExtractorRegistry.extract.mockResolvedValue({ + success: true, + text: sampleTxtCV, + }); + mockParseCVText.mockResolvedValue(mockJohnSmithParsedCV); + + const result = await service.parseFile( + mockUser, + mockBuffer, + mockMimeType, + mockFileName, + ); + + expect(mockTextExtractorRegistry.extract).toHaveBeenCalledWith( + mockBuffer, + mockMimeType, + ); + expect(mockParseCVText).toHaveBeenCalledWith(sampleTxtCV); + expect(result).toEqual(mockJohnSmithParsedCV); + }); + + it("throws error when text extraction fails", async () => { + mockTextExtractorRegistry.extract.mockResolvedValue({ + success: false, + error: "Unsupported file format", + }); + + await expect( + service.parseFile(mockUser, mockBuffer, mockMimeType, mockFileName), + ).rejects.toThrow("Text extraction failed: Unsupported file format"); + }); + + it("throws error when extracted text is empty", async () => { + mockTextExtractorRegistry.extract.mockResolvedValue({ + success: true, + text: "", + }); + + await expect( + service.parseFile(mockUser, mockBuffer, mockMimeType, mockFileName), + ).rejects.toThrow("Could not extract any text from the file"); + }); + + it("throws error when extracted text is only whitespace", async () => { + mockTextExtractorRegistry.extract.mockResolvedValue({ + success: true, + text: " \n\t ", + }); + + await expect( + service.parseFile(mockUser, mockBuffer, mockMimeType, mockFileName), + ).rejects.toThrow("Could not extract any text from the file"); + }); + }); + + describe("parseStoryWithResolution", () => { + beforeEach(() => { + mockParseCVText.mockResolvedValue(mockJohnSmithParsedCV); + + mockEntityResolver.resolveCompany.mockImplementation(async (name) => ({ + id: name === "Acme Corporation" ? "company-1" : null, + name, + })); + + mockEntityResolver.resolveRole.mockImplementation(async (name) => ({ + id: name === "Senior Software Engineer" ? "role-1" : null, + name, + })); + + mockEntityResolver.resolveLevel.mockImplementation(async (name) => ({ + id: name === "Senior" ? "level-1" : null, + name: name ?? "Mid-level", + })); + + mockEntityResolver.resolveInstitution.mockImplementation( + async (name) => ({ + id: name === "Stanford University" ? "institution-1" : null, + name, + }), + ); + + mockEntityResolver.resolveSkills.mockImplementation(async (names) => + names.map((name) => ({ + id: ["TypeScript", "JavaScript"].includes(name) + ? `skill-${name}` + : null, + name, + })), + ); + }); + + it("resolves entities for parsed CV data", async () => { + const result = await service.parseStoryWithResolution( + mockUser, + sampleTxtCV, + ); + + expect(result.jobExperiences).toHaveLength(3); + expect(result.education).toHaveLength(1); + + const firstJob = result.jobExperiences[0]; + expect(firstJob.company.id).toBe("company-1"); + expect(firstJob.company.name).toBe("Acme Corporation"); + expect(firstJob.role.id).toBe("role-1"); + expect(firstJob.level.id).toBe("level-1"); + }); + + it("returns null IDs for unmatched entities", async () => { + const result = await service.parseStoryWithResolution( + mockUser, + sampleTxtCV, + ); + + const secondJob = result.jobExperiences[1]; + expect(secondJob.company.id).toBeNull(); + expect(secondJob.company.name).toBe("TechStart Inc"); + }); + + it("converts date strings to Date objects", async () => { + const result = await service.parseStoryWithResolution( + mockUser, + sampleTxtCV, + ); + + const firstJob = result.jobExperiences[0]; + expect(firstJob.startDate).toBeInstanceOf(Date); + expect(firstJob.endDate).toBeNull(); + + const lastJob = result.jobExperiences[2]; + expect(lastJob.endDate).toBeInstanceOf(Date); + }); + + it("resolves education institutions", async () => { + const result = await service.parseStoryWithResolution( + mockUser, + sampleTxtCV, + ); + + const education = result.education[0]; + expect(education.institution.id).toBe("institution-1"); + expect(education.institution.name).toBe("Stanford University"); + expect(education.degree).toBe("Bachelor of Science"); + }); + + it("resolves skills for both jobs and education", async () => { + const result = await service.parseStoryWithResolution( + mockUser, + sampleTxtCV, + ); + + const firstJob = result.jobExperiences[0]; + const typescriptSkill = firstJob.skills.find( + (s) => s.name === "TypeScript", + ); + expect(typescriptSkill?.id).toBe("skill-TypeScript"); + + const kubernetesSkill = firstJob.skills.find( + (s) => s.name === "Kubernetes", + ); + expect(kubernetesSkill?.id).toBeNull(); + }); + }); +}); diff --git a/apps/api/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts b/apps/api/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts new file mode 100644 index 0000000..19aa6ce --- /dev/null +++ b/apps/api/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts @@ -0,0 +1,324 @@ +import type { Mock, Mocked } from "vitest"; +import { EntityResolverService } from "../entity-resolver.service"; + +type MockPrisma = { + company: { findFirst: Mock }; + role: { findFirst: Mock }; + level: { findFirst: Mock }; + skill: { findFirst: Mock }; + institution: { findFirst: Mock }; +}; + +describe("EntityResolverService", () => { + let service: EntityResolverService; + let mockPrisma: MockPrisma; + + beforeEach(() => { + mockPrisma = { + company: { findFirst: vi.fn() }, + role: { findFirst: vi.fn() }, + level: { findFirst: vi.fn() }, + skill: { findFirst: vi.fn() }, + institution: { findFirst: vi.fn() }, + }; + + service = new EntityResolverService(mockPrisma as any); + }); + + describe("resolveCompany", () => { + it("returns matched company with ID when found", async () => { + const mockCompany = { id: "company-123", name: "Acme Corporation" }; + mockPrisma.company.findFirst.mockResolvedValue(mockCompany); + + const result = await service.resolveCompany("Acme Corporation"); + + expect(mockPrisma.company.findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "Acme Corporation", mode: "insensitive" } }, + }); + expect(result).toEqual({ id: "company-123", name: "Acme Corporation" }); + }); + + it("returns draft entity with null ID when not found", async () => { + mockPrisma.company.findFirst.mockResolvedValue(null); + + const result = await service.resolveCompany("Unknown Company"); + + expect(result).toEqual({ id: null, name: "Unknown Company" }); + }); + + it("performs case-insensitive matching", async () => { + const mockCompany = { id: "company-123", name: "ACME Corporation" }; + mockPrisma.company.findFirst.mockResolvedValue(mockCompany); + + const result = await service.resolveCompany("acme corporation"); + + expect(mockPrisma.company.findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "acme corporation", mode: "insensitive" } }, + }); + expect(result.id).toBe("company-123"); + expect(result.name).toBe("ACME Corporation"); + }); + }); + + describe("resolveRole", () => { + it("returns matched role with ID when found", async () => { + const mockRole = { id: "role-456", name: "Senior Software Engineer" }; + mockPrisma.role.findFirst.mockResolvedValue(mockRole); + + const result = await service.resolveRole("Senior Software Engineer"); + + expect(mockPrisma.role.findFirst).toHaveBeenCalledWith({ + where: { + name: { equals: "Senior Software Engineer", mode: "insensitive" }, + }, + }); + expect(result).toEqual({ + id: "role-456", + name: "Senior Software Engineer", + }); + }); + + it("returns draft entity with null ID when not found", async () => { + mockPrisma.role.findFirst.mockResolvedValue(null); + + const result = await service.resolveRole("Chief Happiness Officer"); + + expect(result).toEqual({ id: null, name: "Chief Happiness Officer" }); + }); + }); + + describe("resolveLevel", () => { + it("returns matched level with ID when found", async () => { + const mockLevel = { id: "level-789", name: "Senior" }; + mockPrisma.level.findFirst.mockResolvedValue(mockLevel); + + const result = await service.resolveLevel("Senior"); + + expect(mockPrisma.level.findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "Senior", mode: "insensitive" } }, + }); + expect(result).toEqual({ id: "level-789", name: "Senior" }); + }); + + it("defaults to Mid-level when no name provided", async () => { + const mockLevel = { id: "level-mid", name: "Mid-level" }; + mockPrisma.level.findFirst.mockResolvedValue(mockLevel); + + const result = await service.resolveLevel(undefined); + + expect(mockPrisma.level.findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "Mid-level", mode: "insensitive" } }, + }); + expect(result).toEqual({ id: "level-mid", name: "Mid-level" }); + }); + + it("defaults to Mid-level when empty string provided", async () => { + mockPrisma.level.findFirst.mockResolvedValue(null); + + const result = await service.resolveLevel(""); + + expect(mockPrisma.level.findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "Mid-level", mode: "insensitive" } }, + }); + expect(result).toEqual({ id: null, name: "Mid-level" }); + }); + + it("returns draft entity with null ID when level not found", async () => { + mockPrisma.level.findFirst.mockResolvedValue(null); + + const result = await service.resolveLevel("Principal"); + + expect(result).toEqual({ id: null, name: "Principal" }); + }); + }); + + describe("resolveSkill", () => { + it("returns matched skill with ID when found", async () => { + const mockSkill = { id: "skill-ts", name: "TypeScript" }; + mockPrisma.skill.findFirst.mockResolvedValue(mockSkill); + + const result = await service.resolveSkill("TypeScript"); + + expect(mockPrisma.skill.findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "TypeScript", mode: "insensitive" } }, + }); + expect(result).toEqual({ id: "skill-ts", name: "TypeScript" }); + }); + + it("returns draft entity with null ID when not found", async () => { + mockPrisma.skill.findFirst.mockResolvedValue(null); + + const result = await service.resolveSkill("ObscureFramework"); + + expect(result).toEqual({ id: null, name: "ObscureFramework" }); + }); + + it("trims whitespace from skill name", async () => { + const mockSkill = { id: "skill-ts", name: "TypeScript" }; + mockPrisma.skill.findFirst.mockResolvedValue(mockSkill); + + const result = await service.resolveSkill(" TypeScript "); + + expect(mockPrisma.skill.findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "TypeScript", mode: "insensitive" } }, + }); + expect(result).toEqual({ id: "skill-ts", name: "TypeScript" }); + }); + + it("returns empty draft entity for empty/whitespace-only input", async () => { + const result = await service.resolveSkill(" "); + + expect(mockPrisma.skill.findFirst).not.toHaveBeenCalled(); + expect(result).toEqual({ id: null, name: "" }); + }); + }); + + describe("resolveInstitution", () => { + it("returns matched institution with ID when found", async () => { + const mockInstitution = { + id: "inst-stanford", + name: "Stanford University", + }; + mockPrisma.institution.findFirst.mockResolvedValue(mockInstitution); + + const result = await service.resolveInstitution("Stanford University"); + + expect(mockPrisma.institution.findFirst).toHaveBeenCalledWith({ + where: { name: { equals: "Stanford University", mode: "insensitive" } }, + }); + expect(result).toEqual({ + id: "inst-stanford", + name: "Stanford University", + }); + }); + + it("returns draft entity with null ID when not found", async () => { + mockPrisma.institution.findFirst.mockResolvedValue(null); + + const result = await service.resolveInstitution("Unknown University"); + + expect(result).toEqual({ id: null, name: "Unknown University" }); + }); + }); + + describe("resolveSkills", () => { + it("resolves multiple skills in parallel", async () => { + mockPrisma.skill.findFirst + .mockResolvedValueOnce({ id: "skill-ts", name: "TypeScript" }) + .mockResolvedValueOnce({ id: "skill-js", name: "JavaScript" }) + .mockResolvedValueOnce(null); + + const result = await service.resolveSkills([ + "TypeScript", + "JavaScript", + "Rust", + ]); + + expect(mockPrisma.skill.findFirst).toHaveBeenCalledTimes(3); + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ id: "skill-ts", name: "TypeScript" }); + expect(result[1]).toEqual({ id: "skill-js", name: "JavaScript" }); + expect(result[2]).toEqual({ id: null, name: "Rust" }); + }); + + it("deduplicates skill names before resolving", async () => { + mockPrisma.skill.findFirst.mockResolvedValue({ + id: "skill-ts", + name: "TypeScript", + }); + + const result = await service.resolveSkills([ + "TypeScript", + "TypeScript", + "TypeScript", + ]); + + expect(mockPrisma.skill.findFirst).toHaveBeenCalledTimes(1); + expect(result).toHaveLength(1); + }); + + it("filters out empty and whitespace-only skill names", async () => { + mockPrisma.skill.findFirst.mockResolvedValue({ + id: "skill-ts", + name: "TypeScript", + }); + + const result = await service.resolveSkills([ + "TypeScript", + "", + " ", + null as unknown as string, + undefined as unknown as string, + ]); + + expect(mockPrisma.skill.findFirst).toHaveBeenCalledTimes(1); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ id: "skill-ts", name: "TypeScript" }); + }); + + it("returns empty array for empty input", async () => { + const result = await service.resolveSkills([]); + + expect(mockPrisma.skill.findFirst).not.toHaveBeenCalled(); + expect(result).toEqual([]); + }); + + it("handles mixed matching and non-matching skills", async () => { + mockPrisma.skill.findFirst + .mockResolvedValueOnce({ id: "skill-1", name: "React" }) + .mockResolvedValueOnce(null) + .mockResolvedValueOnce({ id: "skill-2", name: "Node.js" }) + .mockResolvedValueOnce(null); + + const result = await service.resolveSkills([ + "React", + "SomeObscureLib", + "Node.js", + "AnotherRareSkill", + ]); + + expect(result).toHaveLength(4); + expect(result[0].id).toBe("skill-1"); + expect(result[1].id).toBeNull(); + expect(result[2].id).toBe("skill-2"); + expect(result[3].id).toBeNull(); + }); + }); + + describe("fuzzy matching scenarios", () => { + it("matches company names case-insensitively", async () => { + const mockCompany = { id: "c-1", name: "Google LLC" }; + mockPrisma.company.findFirst.mockResolvedValue(mockCompany); + + const variations = [ + "google llc", + "GOOGLE LLC", + "Google LLC", + "GoOgLe LlC", + ]; + + for (const variation of variations) { + const result = await service.resolveCompany(variation); + expect(result.id).toBe("c-1"); + } + }); + + it("handles abbreviated vs full company names", async () => { + mockPrisma.company.findFirst.mockResolvedValue(null); + + const result = await service.resolveCompany("IBM"); + + expect(result.id).toBeNull(); + expect(result.name).toBe("IBM"); + }); + + it("preserves original name from database when matched", async () => { + const mockSkill = { id: "skill-1", name: "Node.js" }; + mockPrisma.skill.findFirst.mockResolvedValue(mockSkill); + + const result = await service.resolveSkill("nodejs"); + + expect(result.name).toBe("Node.js"); + }); + }); +}); diff --git a/apps/server/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts b/apps/api/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts similarity index 100% rename from apps/server/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts rename to apps/api/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts diff --git a/apps/server/src/modules/cv-parser/__tests__/fixtures/sample-cv.md b/apps/api/src/modules/cv-parser/__tests__/fixtures/sample-cv.md similarity index 100% rename from apps/server/src/modules/cv-parser/__tests__/fixtures/sample-cv.md rename to apps/api/src/modules/cv-parser/__tests__/fixtures/sample-cv.md diff --git a/apps/server/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt b/apps/api/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt similarity index 100% rename from apps/server/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt rename to apps/api/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt diff --git a/apps/server/src/modules/cv-parser/ai-provider-resolver.service.ts b/apps/api/src/modules/cv-parser/ai-provider-resolver.service.ts similarity index 98% rename from apps/server/src/modules/cv-parser/ai-provider-resolver.service.ts rename to apps/api/src/modules/cv-parser/ai-provider-resolver.service.ts index b5ef6f3..e00c769 100644 --- a/apps/server/src/modules/cv-parser/ai-provider-resolver.service.ts +++ b/apps/api/src/modules/cv-parser/ai-provider-resolver.service.ts @@ -5,7 +5,7 @@ import { AnthropicProvider, OpenAIProvider, } from "@cv/ai-provider"; -import type { User } from "@cv/auth"; +import type { User } from "@cv/core"; import { BadRequestException, ForbiddenException, diff --git a/apps/server/src/modules/cv-parser/cv-parser.module.ts b/apps/api/src/modules/cv-parser/cv-parser.module.ts similarity index 98% rename from apps/server/src/modules/cv-parser/cv-parser.module.ts rename to apps/api/src/modules/cv-parser/cv-parser.module.ts index 69b6512..f93506a 100644 --- a/apps/server/src/modules/cv-parser/cv-parser.module.ts +++ b/apps/api/src/modules/cv-parser/cv-parser.module.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { CVParserModule as CVParserCoreModule } from "@cv/ai-parser"; import { AIModule } from "@cv/ai-provider"; import { FileExtractionModule } from "@cv/file-upload"; -import { DatabaseModule } from "@cv/system"; +import { DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { MulterModule } from "@nestjs/platform-express"; import { diskStorage } from "multer"; diff --git a/apps/server/src/modules/cv-parser/cv-parser.resolver.ts b/apps/api/src/modules/cv-parser/cv-parser.resolver.ts similarity index 95% rename from apps/server/src/modules/cv-parser/cv-parser.resolver.ts rename to apps/api/src/modules/cv-parser/cv-parser.resolver.ts index e0ccbdb..cc3c975 100644 --- a/apps/server/src/modules/cv-parser/cv-parser.resolver.ts +++ b/apps/api/src/modules/cv-parser/cv-parser.resolver.ts @@ -1,4 +1,4 @@ -import type { User as DomainUser } from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; import { JwtAuthGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Resolver } from "@nestjs/graphql"; diff --git a/apps/server/src/modules/cv-parser/cv-parser.service.ts b/apps/api/src/modules/cv-parser/cv-parser.service.ts similarity index 99% rename from apps/server/src/modules/cv-parser/cv-parser.service.ts rename to apps/api/src/modules/cv-parser/cv-parser.service.ts index ad4a4aa..c5ee4f7 100644 --- a/apps/server/src/modules/cv-parser/cv-parser.service.ts +++ b/apps/api/src/modules/cv-parser/cv-parser.service.ts @@ -1,5 +1,5 @@ import { CVParserService as CVParser, type ParsedCVData } from "@cv/ai-parser"; -import type { User } from "@cv/auth"; +import type { User } from "@cv/core"; import { TEXT_EXTRACTOR_REGISTRY, type TextExtractorRegistry, diff --git a/apps/server/src/modules/cv-parser/entity-resolver.service.ts b/apps/api/src/modules/cv-parser/entity-resolver.service.ts similarity index 98% rename from apps/server/src/modules/cv-parser/entity-resolver.service.ts rename to apps/api/src/modules/cv-parser/entity-resolver.service.ts index d0e3d08..88a1a94 100644 --- a/apps/server/src/modules/cv-parser/entity-resolver.service.ts +++ b/apps/api/src/modules/cv-parser/entity-resolver.service.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; /** diff --git a/apps/server/src/modules/cv-parser/file-upload.controller.ts b/apps/api/src/modules/cv-parser/file-upload.controller.ts similarity index 98% rename from apps/server/src/modules/cv-parser/file-upload.controller.ts rename to apps/api/src/modules/cv-parser/file-upload.controller.ts index 3e81430..8ceff7f 100644 --- a/apps/server/src/modules/cv-parser/file-upload.controller.ts +++ b/apps/api/src/modules/cv-parser/file-upload.controller.ts @@ -1,6 +1,6 @@ import { createHash } from "node:crypto"; import { readFile, unlink } from "node:fs/promises"; -import type { User as DomainUser } from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; import { JwtAuthGuard } from "@cv/auth"; import { validateFile } from "@cv/file-upload"; import { diff --git a/apps/server/src/modules/cv-parser/graphql/upload-file.input.ts b/apps/api/src/modules/cv-parser/graphql/upload-file.input.ts similarity index 100% rename from apps/server/src/modules/cv-parser/graphql/upload-file.input.ts rename to apps/api/src/modules/cv-parser/graphql/upload-file.input.ts diff --git a/apps/server/src/modules/cv-parser/graphql/upload-file.resolver.ts b/apps/api/src/modules/cv-parser/graphql/upload-file.resolver.ts similarity index 97% rename from apps/server/src/modules/cv-parser/graphql/upload-file.resolver.ts rename to apps/api/src/modules/cv-parser/graphql/upload-file.resolver.ts index 9c0b95a..5d735e1 100644 --- a/apps/server/src/modules/cv-parser/graphql/upload-file.resolver.ts +++ b/apps/api/src/modules/cv-parser/graphql/upload-file.resolver.ts @@ -1,5 +1,5 @@ import { createHash } from "node:crypto"; -import type { User as DomainUser } from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { validateFile } from "@cv/file-upload"; import { BadRequestException, UseGuards } from "@nestjs/common"; diff --git a/apps/server/src/modules/cv-parser/onboarding/import.step.ts b/apps/api/src/modules/cv-parser/onboarding/import.step.ts similarity index 92% rename from apps/server/src/modules/cv-parser/onboarding/import.step.ts rename to apps/api/src/modules/cv-parser/onboarding/import.step.ts index f6e7e6c..1db3fdf 100644 --- a/apps/server/src/modules/cv-parser/onboarding/import.step.ts +++ b/apps/api/src/modules/cv-parser/onboarding/import.step.ts @@ -1,5 +1,5 @@ -import type { User } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { OnboardingStep } from "@/modules/onboarding/onboarding-step.decorator"; import { diff --git a/apps/server/src/modules/cv-parser/types/draft.types.ts b/apps/api/src/modules/cv-parser/types/draft.types.ts similarity index 100% rename from apps/server/src/modules/cv-parser/types/draft.types.ts rename to apps/api/src/modules/cv-parser/types/draft.types.ts diff --git a/apps/server/src/modules/cv-template/cv-data-assembler.service.ts b/apps/api/src/modules/cv-template/cv-data-assembler.service.ts similarity index 96% rename from apps/server/src/modules/cv-template/cv-data-assembler.service.ts rename to apps/api/src/modules/cv-template/cv-data-assembler.service.ts index d30448a..877cdf2 100644 --- a/apps/server/src/modules/cv-template/cv-data-assembler.service.ts +++ b/apps/api/src/modules/cv-template/cv-data-assembler.service.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { ProfileService } from "@/modules/profile/profile.service"; @@ -8,8 +8,7 @@ export interface CVRenderContext { name: string; headline: string | null; phone: string | null; - city: string | null; - country: string | null; + locationId: string | null; website: string | null; linkedInUrl: string | null; summary: string | null; @@ -131,8 +130,7 @@ export class CVDataAssemblerService { name: profile.fullName ?? profile.name, headline: profile.headline ?? null, phone: profile.phone ?? null, - city: profile.city ?? null, - country: profile.country ?? null, + locationId: profile.locationId ?? null, website: profile.website ?? null, linkedInUrl: profile.linkedInUrl ?? null, summary: profile.summary ?? null, diff --git a/apps/server/src/modules/cv-template/cv-renderer.service.ts b/apps/api/src/modules/cv-template/cv-renderer.service.ts similarity index 100% rename from apps/server/src/modules/cv-template/cv-renderer.service.ts rename to apps/api/src/modules/cv-template/cv-renderer.service.ts diff --git a/apps/server/src/modules/cv-template/cv-template.mapper.ts b/apps/api/src/modules/cv-template/cv-template.mapper.ts similarity index 100% rename from apps/server/src/modules/cv-template/cv-template.mapper.ts rename to apps/api/src/modules/cv-template/cv-template.mapper.ts diff --git a/apps/server/src/modules/cv-template/cv-template.module.ts b/apps/api/src/modules/cv-template/cv-template.module.ts similarity index 93% rename from apps/server/src/modules/cv-template/cv-template.module.ts rename to apps/api/src/modules/cv-template/cv-template.module.ts index 2906bee..c183c18 100644 --- a/apps/server/src/modules/cv-template/cv-template.module.ts +++ b/apps/api/src/modules/cv-template/cv-template.module.ts @@ -1,5 +1,5 @@ -import { AuthorizationModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module, forwardRef } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { ProfileModule } from "@/modules/profile/profile.module"; diff --git a/apps/server/src/modules/cv-template/cv-template.policy.ts b/apps/api/src/modules/cv-template/cv-template.policy.ts similarity index 78% rename from apps/server/src/modules/cv-template/cv-template.policy.ts rename to apps/api/src/modules/cv-template/cv-template.policy.ts index 704b93d..22eb7dc 100644 --- a/apps/server/src/modules/cv-template/cv-template.policy.ts +++ b/apps/api/src/modules/cv-template/cv-template.policy.ts @@ -1,4 +1,4 @@ -import { Policy, PublicResourcePolicy } from "@cv/auth"; +import { Policy, PublicResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { CVTemplate } from "./graphql/cv-template.type"; diff --git a/apps/server/src/modules/cv-template/cv-template.service.ts b/apps/api/src/modules/cv-template/cv-template.service.ts similarity index 91% rename from apps/server/src/modules/cv-template/cv-template.service.ts rename to apps/api/src/modules/cv-template/cv-template.service.ts index 08edcc3..b0f6133 100644 --- a/apps/server/src/modules/cv-template/cv-template.service.ts +++ b/apps/api/src/modules/cv-template/cv-template.service.ts @@ -1,5 +1,5 @@ -import { notFound } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import { notFound } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { cvTemplateMapper } from "./cv-template.mapper"; diff --git a/apps/server/src/modules/cv-template/cv.dataloader.ts b/apps/api/src/modules/cv-template/cv.dataloader.ts similarity index 91% rename from apps/server/src/modules/cv-template/cv.dataloader.ts rename to apps/api/src/modules/cv-template/cv.dataloader.ts index 0e01da3..9c9fa0f 100644 --- a/apps/server/src/modules/cv-template/cv.dataloader.ts +++ b/apps/api/src/modules/cv-template/cv.dataloader.ts @@ -1,4 +1,4 @@ -import { BaseDataLoaderService } from "@cv/system"; +import { BaseDataLoaderService } from "@cv/core"; import { Injectable, Scope } from "@nestjs/common"; import { CVService } from "./cv.service"; import { CV } from "./graphql/cv.type"; diff --git a/apps/server/src/modules/cv-template/cv.mapper.ts b/apps/api/src/modules/cv-template/cv.mapper.ts similarity index 100% rename from apps/server/src/modules/cv-template/cv.mapper.ts rename to apps/api/src/modules/cv-template/cv.mapper.ts diff --git a/apps/server/src/modules/cv-template/cv.policy.ts b/apps/api/src/modules/cv-template/cv.policy.ts similarity index 69% rename from apps/server/src/modules/cv-template/cv.policy.ts rename to apps/api/src/modules/cv-template/cv.policy.ts index 099e0b4..329c9e2 100644 --- a/apps/server/src/modules/cv-template/cv.policy.ts +++ b/apps/api/src/modules/cv-template/cv.policy.ts @@ -1,5 +1,5 @@ -import { Policy, ProfileOwnedResourcePolicy } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import { Policy, ProfileOwnedResourcePolicy } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { CV } from "./graphql/cv.type"; diff --git a/apps/server/src/modules/cv-template/cv.service.ts b/apps/api/src/modules/cv-template/cv.service.ts similarity index 96% rename from apps/server/src/modules/cv-template/cv.service.ts rename to apps/api/src/modules/cv-template/cv.service.ts index 070bcad..e5233e1 100644 --- a/apps/server/src/modules/cv-template/cv.service.ts +++ b/apps/api/src/modules/cv-template/cv.service.ts @@ -1,6 +1,6 @@ -import { notFound } from "@cv/auth"; -import type { PaginationOptions, PaginationResult } from "@cv/system"; -import { type EntityService, PaginationService, PrismaService } from "@cv/system"; +import { notFound } from "@cv/core"; +import type { PaginationOptions, PaginationResult } from "@cv/core"; +import { type EntityService, PaginationService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Prisma } from "@prisma/client"; import { cvMapper } from "./cv.mapper"; diff --git a/apps/server/src/modules/cv-template/graphql/cv-args.type.ts b/apps/api/src/modules/cv-template/graphql/cv-args.type.ts similarity index 100% rename from apps/server/src/modules/cv-template/graphql/cv-args.type.ts rename to apps/api/src/modules/cv-template/graphql/cv-args.type.ts diff --git a/apps/server/src/modules/cv-template/graphql/cv-input.type.ts b/apps/api/src/modules/cv-template/graphql/cv-input.type.ts similarity index 100% rename from apps/server/src/modules/cv-template/graphql/cv-input.type.ts rename to apps/api/src/modules/cv-template/graphql/cv-input.type.ts diff --git a/apps/server/src/modules/cv-template/graphql/cv-template-args.type.ts b/apps/api/src/modules/cv-template/graphql/cv-template-args.type.ts similarity index 100% rename from apps/server/src/modules/cv-template/graphql/cv-template-args.type.ts rename to apps/api/src/modules/cv-template/graphql/cv-template-args.type.ts diff --git a/apps/server/src/modules/cv-template/graphql/cv-template.resolver.ts b/apps/api/src/modules/cv-template/graphql/cv-template.resolver.ts similarity index 95% rename from apps/server/src/modules/cv-template/graphql/cv-template.resolver.ts rename to apps/api/src/modules/cv-template/graphql/cv-template.resolver.ts index 1465487..6f46958 100644 --- a/apps/server/src/modules/cv-template/graphql/cv-template.resolver.ts +++ b/apps/api/src/modules/cv-template/graphql/cv-template.resolver.ts @@ -1,10 +1,7 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; -import { PaginationService, UuidFactoryService } from "@cv/system"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; +import { PaginationService, UuidFactoryService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import type { MessageBus } from "@riotbyte-com/project-q-core"; diff --git a/apps/server/src/modules/cv-template/graphql/cv-template.type.ts b/apps/api/src/modules/cv-template/graphql/cv-template.type.ts similarity index 97% rename from apps/server/src/modules/cv-template/graphql/cv-template.type.ts rename to apps/api/src/modules/cv-template/graphql/cv-template.type.ts index 215c6bd..f23440a 100644 --- a/apps/server/src/modules/cv-template/graphql/cv-template.type.ts +++ b/apps/api/src/modules/cv-template/graphql/cv-template.type.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; import { Field, ID, ObjectType } from "@nestjs/graphql"; import { createConnection } from "@/modules/base/connection.factory"; diff --git a/apps/server/src/modules/cv-template/graphql/cv.type.ts b/apps/api/src/modules/cv-template/graphql/cv.type.ts similarity index 96% rename from apps/server/src/modules/cv-template/graphql/cv.type.ts rename to apps/api/src/modules/cv-template/graphql/cv.type.ts index 20354e4..ae47578 100644 --- a/apps/server/src/modules/cv-template/graphql/cv.type.ts +++ b/apps/api/src/modules/cv-template/graphql/cv.type.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; import { Field, ID, ObjectType } from "@nestjs/graphql"; import { createConnection } from "@/modules/base/connection.factory"; import { CVTemplate } from "./cv-template.type"; diff --git a/apps/server/src/modules/cv-template/graphql/user-field.resolver.ts b/apps/api/src/modules/cv-template/graphql/user-field.resolver.ts similarity index 95% rename from apps/server/src/modules/cv-template/graphql/user-field.resolver.ts rename to apps/api/src/modules/cv-template/graphql/user-field.resolver.ts index 832e970..9526a6e 100644 --- a/apps/server/src/modules/cv-template/graphql/user-field.resolver.ts +++ b/apps/api/src/modules/cv-template/graphql/user-field.resolver.ts @@ -1,5 +1,5 @@ import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; -import { PaginationService, PrismaService } from "@cv/system"; +import { PaginationService, PrismaService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Parent, ResolveField, Resolver } from "@nestjs/graphql"; import { User } from "@/modules/user/user.type"; diff --git a/apps/server/src/modules/cv-template/pdf-download.controller.ts b/apps/api/src/modules/cv-template/pdf-download.controller.ts similarity index 90% rename from apps/server/src/modules/cv-template/pdf-download.controller.ts rename to apps/api/src/modules/cv-template/pdf-download.controller.ts index 3860fca..c734ae8 100644 --- a/apps/server/src/modules/cv-template/pdf-download.controller.ts +++ b/apps/api/src/modules/cv-template/pdf-download.controller.ts @@ -1,5 +1,6 @@ -import type { User as DomainUser } from "@cv/auth"; -import { AuthorizationService, JwtAuthGuard } from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard } from "@cv/auth"; import { FILE_STORAGE, type FileStorage } from "@cv/file-storage"; import { Controller, diff --git a/apps/server/src/modules/cv-template/seed/cv-template.seed.ts b/apps/api/src/modules/cv-template/seed/cv-template.seed.ts similarity index 97% rename from apps/server/src/modules/cv-template/seed/cv-template.seed.ts rename to apps/api/src/modules/cv-template/seed/cv-template.seed.ts index d7b0248..c58f66b 100644 --- a/apps/server/src/modules/cv-template/seed/cv-template.seed.ts +++ b/apps/api/src/modules/cv-template/seed/cv-template.seed.ts @@ -1,6 +1,6 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { Injectable, Logger } from "@nestjs/common"; import { Seeder } from "@/modules/database/seed/seed.service"; import { Seeder as SeederDecorator } from "@/modules/database/seed/seeder.decorator"; diff --git a/apps/server/src/modules/cv-template/seed/templates/classic-executive.css b/apps/api/src/modules/cv-template/seed/templates/classic-executive.css similarity index 100% rename from apps/server/src/modules/cv-template/seed/templates/classic-executive.css rename to apps/api/src/modules/cv-template/seed/templates/classic-executive.css diff --git a/apps/server/src/modules/cv-template/seed/templates/classic-executive.hbs b/apps/api/src/modules/cv-template/seed/templates/classic-executive.hbs similarity index 100% rename from apps/server/src/modules/cv-template/seed/templates/classic-executive.hbs rename to apps/api/src/modules/cv-template/seed/templates/classic-executive.hbs diff --git a/apps/server/src/modules/cv-template/seed/templates/creative-portfolio.css b/apps/api/src/modules/cv-template/seed/templates/creative-portfolio.css similarity index 100% rename from apps/server/src/modules/cv-template/seed/templates/creative-portfolio.css rename to apps/api/src/modules/cv-template/seed/templates/creative-portfolio.css diff --git a/apps/server/src/modules/cv-template/seed/templates/creative-portfolio.hbs b/apps/api/src/modules/cv-template/seed/templates/creative-portfolio.hbs similarity index 100% rename from apps/server/src/modules/cv-template/seed/templates/creative-portfolio.hbs rename to apps/api/src/modules/cv-template/seed/templates/creative-portfolio.hbs diff --git a/apps/server/src/modules/cv-template/seed/templates/modern-professional.css b/apps/api/src/modules/cv-template/seed/templates/modern-professional.css similarity index 100% rename from apps/server/src/modules/cv-template/seed/templates/modern-professional.css rename to apps/api/src/modules/cv-template/seed/templates/modern-professional.css diff --git a/apps/server/src/modules/cv-template/seed/templates/modern-professional.hbs b/apps/api/src/modules/cv-template/seed/templates/modern-professional.hbs similarity index 100% rename from apps/server/src/modules/cv-template/seed/templates/modern-professional.hbs rename to apps/api/src/modules/cv-template/seed/templates/modern-professional.hbs diff --git a/apps/server/src/modules/data-import/data-import-source.interface.ts b/apps/api/src/modules/data-import/data-import-source.interface.ts similarity index 87% rename from apps/server/src/modules/data-import/data-import-source.interface.ts rename to apps/api/src/modules/data-import/data-import-source.interface.ts index 4fe87e3..831a716 100644 --- a/apps/server/src/modules/data-import/data-import-source.interface.ts +++ b/apps/api/src/modules/data-import/data-import-source.interface.ts @@ -1,5 +1,5 @@ import type { ParsedCVData } from "@cv/ai-parser"; -import type { User } from "@cv/auth"; +import type { User } from "@cv/core"; export interface DataImportSource { readonly name: string; diff --git a/apps/server/src/modules/data-import/data-import.module.ts b/apps/api/src/modules/data-import/data-import.module.ts similarity index 78% rename from apps/server/src/modules/data-import/data-import.module.ts rename to apps/api/src/modules/data-import/data-import.module.ts index a2210e8..2097d01 100644 --- a/apps/server/src/modules/data-import/data-import.module.ts +++ b/apps/api/src/modules/data-import/data-import.module.ts @@ -1,7 +1,6 @@ -import { AuthorizationModule } from "@cv/auth"; -import { DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; -import { EventEmitterModule } from "@nestjs/event-emitter"; import { EntityResolverService } from "@/modules/cv-parser/entity-resolver.service"; import { ProfileModule } from "@/modules/profile/profile.module"; import { UserFileResolver } from "./graphql/user-file.resolver"; @@ -13,7 +12,7 @@ import { UserFileMapper } from "./user-file.mapper"; import { UserFilePolicy } from "./user-file.policy"; @Module({ - imports: [DatabaseModule, AuthorizationModule, EventEmitterModule.forRoot(), ProfileModule], + imports: [DatabaseModule, AuthorizationModule, ProfileModule], providers: [ ImportService, EntityResolverService, diff --git a/apps/api/src/modules/data-import/events/user-file-created.event.ts b/apps/api/src/modules/data-import/events/user-file-created.event.ts new file mode 100644 index 0000000..b3ea596 --- /dev/null +++ b/apps/api/src/modules/data-import/events/user-file-created.event.ts @@ -0,0 +1,12 @@ +import { DomainEvent } from "@cv/core"; +import type { User } from "@cv/core"; +import type { DataImportSource } from "../data-import-source.interface"; + +interface UserFileCreatedPayload { + userFileId: string; + user: User; + source: DataImportSource; + params: Record; +} + +export class UserFileCreatedEvent extends DomainEvent {} diff --git a/apps/server/src/modules/data-import/graphql/user-file.resolver.ts b/apps/api/src/modules/data-import/graphql/user-file.resolver.ts similarity index 90% rename from apps/server/src/modules/data-import/graphql/user-file.resolver.ts rename to apps/api/src/modules/data-import/graphql/user-file.resolver.ts index 476fc3e..147bf79 100644 --- a/apps/server/src/modules/data-import/graphql/user-file.resolver.ts +++ b/apps/api/src/modules/data-import/graphql/user-file.resolver.ts @@ -1,9 +1,6 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; diff --git a/apps/server/src/modules/data-import/graphql/user-file.type.ts b/apps/api/src/modules/data-import/graphql/user-file.type.ts similarity index 100% rename from apps/server/src/modules/data-import/graphql/user-file.type.ts rename to apps/api/src/modules/data-import/graphql/user-file.type.ts diff --git a/apps/server/src/modules/data-import/import-job.entity.ts b/apps/api/src/modules/data-import/import-job.entity.ts similarity index 91% rename from apps/server/src/modules/data-import/import-job.entity.ts rename to apps/api/src/modules/data-import/import-job.entity.ts index 48d1bed..680b547 100644 --- a/apps/server/src/modules/data-import/import-job.entity.ts +++ b/apps/api/src/modules/data-import/import-job.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; export class ImportJob extends BaseEntity { constructor( diff --git a/apps/server/src/modules/data-import/import-job.mapper.ts b/apps/api/src/modules/data-import/import-job.mapper.ts similarity index 95% rename from apps/server/src/modules/data-import/import-job.mapper.ts rename to apps/api/src/modules/data-import/import-job.mapper.ts index 7b34657..79bd512 100644 --- a/apps/server/src/modules/data-import/import-job.mapper.ts +++ b/apps/api/src/modules/data-import/import-job.mapper.ts @@ -1,4 +1,4 @@ -import type { BaseMapper } from "@cv/system"; +import type { BaseMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Prisma } from "@prisma/client"; diff --git a/apps/server/src/modules/data-import/import-job.policy.ts b/apps/api/src/modules/data-import/import-job.policy.ts similarity index 88% rename from apps/server/src/modules/data-import/import-job.policy.ts rename to apps/api/src/modules/data-import/import-job.policy.ts index 9ff7041..1fc7f68 100644 --- a/apps/server/src/modules/data-import/import-job.policy.ts +++ b/apps/api/src/modules/data-import/import-job.policy.ts @@ -1,5 +1,5 @@ -import type { IPolicy, User } from "@cv/auth"; -import { Policy } from "@cv/auth"; +import type { IPolicy, User } from "@cv/core"; +import { Policy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { ImportJob } from "./import-job.entity"; diff --git a/apps/server/src/modules/data-import/import.service.ts b/apps/api/src/modules/data-import/import.service.ts similarity index 88% rename from apps/server/src/modules/data-import/import.service.ts rename to apps/api/src/modules/data-import/import.service.ts index 4a3673f..f5baefb 100644 --- a/apps/server/src/modules/data-import/import.service.ts +++ b/apps/api/src/modules/data-import/import.service.ts @@ -1,7 +1,7 @@ -import type { User } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { PrismaService } from "@cv/core"; +import { EventService } from "@cv/core"; import { Injectable } from "@nestjs/common"; -import { EventEmitter2 } from "@nestjs/event-emitter"; import type { DataImportSource } from "./data-import-source.interface"; import { UserFileCreatedEvent } from "./events/user-file-created.event"; import { UserFile } from "./user-file.entity"; @@ -12,7 +12,7 @@ export class ImportService { constructor( private readonly prisma: PrismaService, private readonly userFileMapper: UserFileMapper, - private readonly eventEmitter: EventEmitter2, + private readonly eventService: EventService, ) {} /** @@ -56,9 +56,8 @@ export class ImportService { }, }); - this.eventEmitter.emit( - UserFileCreatedEvent.event, - new UserFileCreatedEvent(record.id, user, source, params), + this.eventService.emit( + new UserFileCreatedEvent({ userFileId: record.id, user, source, params }), ); return this.userFileMapper.toDomain(record) as UserFile; diff --git a/apps/server/src/modules/data-import/listeners/import-job.listener.ts b/apps/api/src/modules/data-import/listeners/import-job.listener.ts similarity index 96% rename from apps/server/src/modules/data-import/listeners/import-job.listener.ts rename to apps/api/src/modules/data-import/listeners/import-job.listener.ts index 49ff425..6bb4b66 100644 --- a/apps/server/src/modules/data-import/listeners/import-job.listener.ts +++ b/apps/api/src/modules/data-import/listeners/import-job.listener.ts @@ -1,5 +1,5 @@ import type { ParsedCVData } from "@cv/ai-parser"; -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { Injectable, Logger } from "@nestjs/common"; import { OnEvent } from "@nestjs/event-emitter"; import { @@ -35,12 +35,12 @@ export class ImportJobListener { private readonly profileService: ProfileService, ) {} - @OnEvent(UserFileCreatedEvent.event, { async: true }) + @OnEvent(UserFileCreatedEvent.eventName, { async: true }) async handleUserFileCreated(event: UserFileCreatedEvent): Promise { const job = await this.prisma.importJob.create({ data: { - userFileId: event.userFileId, - source: event.source.name, + userFileId: event.payload.userFileId, + source: event.payload.source.name, status: "pending", statusMessage: "Queued", }, @@ -53,7 +53,7 @@ export class ImportJobListener { jobId: string, event: UserFileCreatedEvent, ): Promise { - const { userFileId, user, source, params } = event; + const { userFileId, user, source, params } = event.payload; const tag = `job=${jobId} source=${source.name}`; try { diff --git a/apps/server/src/modules/data-import/sources/file-import-source.ts b/apps/api/src/modules/data-import/sources/file-import-source.ts similarity index 94% rename from apps/server/src/modules/data-import/sources/file-import-source.ts rename to apps/api/src/modules/data-import/sources/file-import-source.ts index d009395..fa6f575 100644 --- a/apps/server/src/modules/data-import/sources/file-import-source.ts +++ b/apps/api/src/modules/data-import/sources/file-import-source.ts @@ -3,8 +3,8 @@ import { type ExistingUserContext, type ParsedCVData, } from "@cv/ai-parser"; -import type { User } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { TextExtractorRegistry, TEXT_EXTRACTOR_REGISTRY } from "@cv/file-upload"; import { Inject, Injectable, Logger } from "@nestjs/common"; import { EducationService } from "@/modules/education/education.service"; @@ -17,7 +17,7 @@ import type { DataImportSource } from "../data-import-source.interface"; * Build ExistingUserContext from domain models for AI prompt enrichment. */ const buildExistingUserContext = ( - profile: { fullName?: string | null; headline?: string | null; city?: string | null; country?: string | null } | null, + profile: { fullName?: string | null; headline?: string | null } | null, jobs: Array<{ company: { name: string }; role: { name: string }; startDate: Date; endDate?: Date }>, educations: Array<{ institution: { name: string }; degree: string; startDate: Date; endDate: Date | null }>, ): ExistingUserContext | undefined => { @@ -25,8 +25,6 @@ const buildExistingUserContext = ( if (profile?.fullName) context.name = profile.fullName; if (profile?.headline) context.headline = profile.headline; - if (profile?.city) context.city = profile.city; - if (profile?.country) context.country = profile.country; if (jobs.length > 0) { context.jobs = jobs.map((j) => { diff --git a/apps/server/src/modules/data-import/user-file.entity.ts b/apps/api/src/modules/data-import/user-file.entity.ts similarity index 93% rename from apps/server/src/modules/data-import/user-file.entity.ts rename to apps/api/src/modules/data-import/user-file.entity.ts index bdeea5a..859d0f1 100644 --- a/apps/server/src/modules/data-import/user-file.entity.ts +++ b/apps/api/src/modules/data-import/user-file.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; export class UserFile extends BaseEntity { constructor( diff --git a/apps/server/src/modules/data-import/user-file.mapper.ts b/apps/api/src/modules/data-import/user-file.mapper.ts similarity index 95% rename from apps/server/src/modules/data-import/user-file.mapper.ts rename to apps/api/src/modules/data-import/user-file.mapper.ts index 3e494e6..1542dcd 100644 --- a/apps/server/src/modules/data-import/user-file.mapper.ts +++ b/apps/api/src/modules/data-import/user-file.mapper.ts @@ -1,4 +1,4 @@ -import type { BaseMapper } from "@cv/system"; +import type { BaseMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Prisma } from "@prisma/client"; diff --git a/apps/server/src/modules/data-import/user-file.policy.ts b/apps/api/src/modules/data-import/user-file.policy.ts similarity index 71% rename from apps/server/src/modules/data-import/user-file.policy.ts rename to apps/api/src/modules/data-import/user-file.policy.ts index bb0a727..f5c55f2 100644 --- a/apps/server/src/modules/data-import/user-file.policy.ts +++ b/apps/api/src/modules/data-import/user-file.policy.ts @@ -1,5 +1,5 @@ -import { Policy, ProfileOwnedResourcePolicy } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import { Policy, ProfileOwnedResourcePolicy } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { UserFile } from "./user-file.entity"; diff --git a/apps/server/src/modules/database/database.module.ts b/apps/api/src/modules/database/database.module.ts similarity index 100% rename from apps/server/src/modules/database/database.module.ts rename to apps/api/src/modules/database/database.module.ts diff --git a/apps/server/src/modules/database/prisma.service.ts b/apps/api/src/modules/database/prisma.service.ts similarity index 100% rename from apps/server/src/modules/database/prisma.service.ts rename to apps/api/src/modules/database/prisma.service.ts diff --git a/apps/server/src/modules/database/seed/seed.module.ts b/apps/api/src/modules/database/seed/seed.module.ts similarity index 93% rename from apps/server/src/modules/database/seed/seed.module.ts rename to apps/api/src/modules/database/seed/seed.module.ts index cf4e05e..444c812 100644 --- a/apps/server/src/modules/database/seed/seed.module.ts +++ b/apps/api/src/modules/database/seed/seed.module.ts @@ -1,5 +1,5 @@ import { AuthModule } from "@cv/auth"; -import { DatabaseModule } from "@cv/system"; +import { DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { DiscoveryModule } from "@nestjs/core"; diff --git a/apps/server/src/modules/database/seed/seed.service.ts b/apps/api/src/modules/database/seed/seed.service.ts similarity index 98% rename from apps/server/src/modules/database/seed/seed.service.ts rename to apps/api/src/modules/database/seed/seed.service.ts index acaf9d1..61bddba 100644 --- a/apps/server/src/modules/database/seed/seed.service.ts +++ b/apps/api/src/modules/database/seed/seed.service.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; import { DiscoveryService } from "@nestjs/core"; import type { SeederConfig } from "./seeder.decorator"; diff --git a/apps/server/src/modules/database/seed/seeder.decorator.ts b/apps/api/src/modules/database/seed/seeder.decorator.ts similarity index 100% rename from apps/server/src/modules/database/seed/seeder.decorator.ts rename to apps/api/src/modules/database/seed/seeder.decorator.ts diff --git a/apps/server/src/modules/database/seed/topological-sort.spec.ts b/apps/api/src/modules/database/seed/topological-sort.spec.ts similarity index 100% rename from apps/server/src/modules/database/seed/topological-sort.spec.ts rename to apps/api/src/modules/database/seed/topological-sort.spec.ts diff --git a/apps/server/src/modules/database/seed/topological-sort.ts b/apps/api/src/modules/database/seed/topological-sort.ts similarity index 100% rename from apps/server/src/modules/database/seed/topological-sort.ts rename to apps/api/src/modules/database/seed/topological-sort.ts diff --git a/apps/server/src/modules/education/education.entity.ts b/apps/api/src/modules/education/education.entity.ts similarity index 94% rename from apps/server/src/modules/education/education.entity.ts rename to apps/api/src/modules/education/education.entity.ts index dc60d62..176fe3a 100644 --- a/apps/server/src/modules/education/education.entity.ts +++ b/apps/api/src/modules/education/education.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; import type { Skill } from "@/modules/job-experience/skill/skill.entity"; import { Institution } from "./institution.entity"; diff --git a/apps/server/src/modules/education/education.mapper.ts b/apps/api/src/modules/education/education.mapper.ts similarity index 97% rename from apps/server/src/modules/education/education.mapper.ts rename to apps/api/src/modules/education/education.mapper.ts index 995d9ec..37955f2 100644 --- a/apps/server/src/modules/education/education.mapper.ts +++ b/apps/api/src/modules/education/education.mapper.ts @@ -1,4 +1,4 @@ -import type { BaseMapper } from "@cv/system"; +import type { BaseMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Prisma } from "@prisma/client"; import { SkillMapper } from "@/modules/job-experience/skill/skill.mapper"; diff --git a/apps/server/src/modules/education/education.module.ts b/apps/api/src/modules/education/education.module.ts similarity index 93% rename from apps/server/src/modules/education/education.module.ts rename to apps/api/src/modules/education/education.module.ts index 805040d..64c385c 100644 --- a/apps/server/src/modules/education/education.module.ts +++ b/apps/api/src/modules/education/education.module.ts @@ -1,5 +1,5 @@ -import { AuthorizationModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { SkillModule } from "@/modules/job-experience/skill/skill.module"; diff --git a/apps/server/src/modules/education/education.policy.ts b/apps/api/src/modules/education/education.policy.ts similarity index 71% rename from apps/server/src/modules/education/education.policy.ts rename to apps/api/src/modules/education/education.policy.ts index 55607c1..7410da0 100644 --- a/apps/server/src/modules/education/education.policy.ts +++ b/apps/api/src/modules/education/education.policy.ts @@ -1,5 +1,5 @@ -import { Policy, ProfileOwnedResourcePolicy } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import { Policy, ProfileOwnedResourcePolicy } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Education } from "./education.entity"; diff --git a/apps/server/src/modules/education/education.service.ts b/apps/api/src/modules/education/education.service.ts similarity index 96% rename from apps/server/src/modules/education/education.service.ts rename to apps/api/src/modules/education/education.service.ts index 1fb1018..20ac05d 100644 --- a/apps/server/src/modules/education/education.service.ts +++ b/apps/api/src/modules/education/education.service.ts @@ -1,6 +1,6 @@ -import { notFound } from "@cv/auth"; -import type { PaginationOptions, PaginationResult } from "@cv/system"; -import { PaginationService, PrismaService } from "@cv/system"; +import { notFound } from "@cv/core"; +import type { PaginationOptions, PaginationResult } from "@cv/core"; +import { PaginationService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Prisma } from "@prisma/client"; import { Education } from "./education.entity"; diff --git a/apps/server/src/modules/education/graphql/education.resolver.ts b/apps/api/src/modules/education/graphql/education.resolver.ts similarity index 96% rename from apps/server/src/modules/education/graphql/education.resolver.ts rename to apps/api/src/modules/education/graphql/education.resolver.ts index fc3c926..bf02125 100644 --- a/apps/server/src/modules/education/graphql/education.resolver.ts +++ b/apps/api/src/modules/education/graphql/education.resolver.ts @@ -1,9 +1,6 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Resolver } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; diff --git a/apps/server/src/modules/education/graphql/education.type.ts b/apps/api/src/modules/education/graphql/education.type.ts similarity index 100% rename from apps/server/src/modules/education/graphql/education.type.ts rename to apps/api/src/modules/education/graphql/education.type.ts diff --git a/apps/server/src/modules/education/graphql/institution.resolver.ts b/apps/api/src/modules/education/graphql/institution.resolver.ts similarity index 90% rename from apps/server/src/modules/education/graphql/institution.resolver.ts rename to apps/api/src/modules/education/graphql/institution.resolver.ts index 044cc02..3cbae7a 100644 --- a/apps/server/src/modules/education/graphql/institution.resolver.ts +++ b/apps/api/src/modules/education/graphql/institution.resolver.ts @@ -1,9 +1,6 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; diff --git a/apps/server/src/modules/education/graphql/institution.type.ts b/apps/api/src/modules/education/graphql/institution.type.ts similarity index 100% rename from apps/server/src/modules/education/graphql/institution.type.ts rename to apps/api/src/modules/education/graphql/institution.type.ts diff --git a/apps/server/src/modules/education/graphql/user-field.resolver.ts b/apps/api/src/modules/education/graphql/user-field.resolver.ts similarity index 99% rename from apps/server/src/modules/education/graphql/user-field.resolver.ts rename to apps/api/src/modules/education/graphql/user-field.resolver.ts index 8797fc4..b6c7838 100644 --- a/apps/server/src/modules/education/graphql/user-field.resolver.ts +++ b/apps/api/src/modules/education/graphql/user-field.resolver.ts @@ -1,5 +1,5 @@ import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; -import { PaginationArgs, PaginationService, PrismaService } from "@cv/system"; +import { PaginationArgs, PaginationService, PrismaService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Parent, ResolveField, Resolver } from "@nestjs/graphql"; import { User } from "@/modules/user/user.type"; diff --git a/apps/server/src/modules/education/institution.entity.ts b/apps/api/src/modules/education/institution.entity.ts similarity index 53% rename from apps/server/src/modules/education/institution.entity.ts rename to apps/api/src/modules/education/institution.entity.ts index 1f0f42e..e3bbcc8 100644 --- a/apps/server/src/modules/education/institution.entity.ts +++ b/apps/api/src/modules/education/institution.entity.ts @@ -1,3 +1,3 @@ -import { NamedEntity } from "@cv/system"; +import { NamedEntity } from "@cv/core"; export class Institution extends NamedEntity {} diff --git a/apps/server/src/modules/education/institution.factory.ts b/apps/api/src/modules/education/institution.factory.ts similarity index 89% rename from apps/server/src/modules/education/institution.factory.ts rename to apps/api/src/modules/education/institution.factory.ts index 8809e54..c5ebacf 100644 --- a/apps/server/src/modules/education/institution.factory.ts +++ b/apps/api/src/modules/education/institution.factory.ts @@ -1,4 +1,4 @@ -import { ClockService, Factory, UuidFactoryService } from "@cv/system"; +import { ClockService, Factory, UuidFactoryService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Institution } from "./institution.entity"; diff --git a/apps/server/src/modules/education/institution.mapper.ts b/apps/api/src/modules/education/institution.mapper.ts similarity index 93% rename from apps/server/src/modules/education/institution.mapper.ts rename to apps/api/src/modules/education/institution.mapper.ts index 69913fe..4030272 100644 --- a/apps/server/src/modules/education/institution.mapper.ts +++ b/apps/api/src/modules/education/institution.mapper.ts @@ -1,4 +1,4 @@ -import { createNamedEntityMapper } from "@cv/system"; +import { createNamedEntityMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Institution as PrismaInstitution } from "@prisma/client"; import { Institution } from "./institution.entity"; diff --git a/apps/server/src/modules/education/institution.policy.ts b/apps/api/src/modules/education/institution.policy.ts similarity index 82% rename from apps/server/src/modules/education/institution.policy.ts rename to apps/api/src/modules/education/institution.policy.ts index 273d6c5..da54d87 100644 --- a/apps/server/src/modules/education/institution.policy.ts +++ b/apps/api/src/modules/education/institution.policy.ts @@ -1,4 +1,4 @@ -import { Policy, PublicResourcePolicy } from "@cv/auth"; +import { Policy, PublicResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Institution } from "./institution.entity"; diff --git a/apps/server/src/modules/education/institution.service.ts b/apps/api/src/modules/education/institution.service.ts similarity index 86% rename from apps/server/src/modules/education/institution.service.ts rename to apps/api/src/modules/education/institution.service.ts index b48c5fa..6db279a 100644 --- a/apps/server/src/modules/education/institution.service.ts +++ b/apps/api/src/modules/education/institution.service.ts @@ -1,4 +1,4 @@ -import { NamedEntityService, PrismaService } from "@cv/system"; +import { NamedEntityService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Institution } from "./institution.entity"; import { InstitutionMapper } from "./institution.mapper"; diff --git a/apps/server/src/modules/education/onboarding/education.step.ts b/apps/api/src/modules/education/onboarding/education.step.ts similarity index 90% rename from apps/server/src/modules/education/onboarding/education.step.ts rename to apps/api/src/modules/education/onboarding/education.step.ts index a41f12e..db341ac 100644 --- a/apps/server/src/modules/education/onboarding/education.step.ts +++ b/apps/api/src/modules/education/onboarding/education.step.ts @@ -1,5 +1,5 @@ -import type { User } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { OnboardingStep } from "@/modules/onboarding/onboarding-step.decorator"; import { diff --git a/apps/server/src/modules/job-experience/company/company.dataloader.ts b/apps/api/src/modules/job-experience/company/company.dataloader.ts similarity index 92% rename from apps/server/src/modules/job-experience/company/company.dataloader.ts rename to apps/api/src/modules/job-experience/company/company.dataloader.ts index a5ad5c5..4b224fa 100644 --- a/apps/server/src/modules/job-experience/company/company.dataloader.ts +++ b/apps/api/src/modules/job-experience/company/company.dataloader.ts @@ -1,4 +1,4 @@ -import { BaseDataLoaderService } from "@cv/system"; +import { BaseDataLoaderService } from "@cv/core"; import { Injectable, Scope } from "@nestjs/common"; import { Company } from "./company.entity"; import { CompanyService } from "./company.service"; diff --git a/apps/server/src/modules/job-experience/company/company.dto.ts b/apps/api/src/modules/job-experience/company/company.dto.ts similarity index 100% rename from apps/server/src/modules/job-experience/company/company.dto.ts rename to apps/api/src/modules/job-experience/company/company.dto.ts diff --git a/apps/server/src/modules/job-experience/company/company.entity.ts b/apps/api/src/modules/job-experience/company/company.entity.ts similarity index 86% rename from apps/server/src/modules/job-experience/company/company.entity.ts rename to apps/api/src/modules/job-experience/company/company.entity.ts index 52f5799..2301260 100644 --- a/apps/server/src/modules/job-experience/company/company.entity.ts +++ b/apps/api/src/modules/job-experience/company/company.entity.ts @@ -1,4 +1,4 @@ -import { NamedEntity } from "@cv/system"; +import { NamedEntity } from "@cv/core"; export class Company extends NamedEntity { constructor( diff --git a/apps/server/src/modules/job-experience/company/company.factory.ts b/apps/api/src/modules/job-experience/company/company.factory.ts similarity index 89% rename from apps/server/src/modules/job-experience/company/company.factory.ts rename to apps/api/src/modules/job-experience/company/company.factory.ts index 8aac3b3..26dce40 100644 --- a/apps/server/src/modules/job-experience/company/company.factory.ts +++ b/apps/api/src/modules/job-experience/company/company.factory.ts @@ -1,4 +1,4 @@ -import { ClockService, Factory, UuidFactoryService } from "@cv/system"; +import { ClockService, Factory, UuidFactoryService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { CreateCompanyDto } from "./company.dto"; import { Company } from "./company.entity"; diff --git a/apps/server/src/modules/job-experience/company/company.mapper.ts b/apps/api/src/modules/job-experience/company/company.mapper.ts similarity index 100% rename from apps/server/src/modules/job-experience/company/company.mapper.ts rename to apps/api/src/modules/job-experience/company/company.mapper.ts diff --git a/apps/server/src/modules/job-experience/company/company.module.ts b/apps/api/src/modules/job-experience/company/company.module.ts similarity index 89% rename from apps/server/src/modules/job-experience/company/company.module.ts rename to apps/api/src/modules/job-experience/company/company.module.ts index 30da4ab..3ef0282 100644 --- a/apps/server/src/modules/job-experience/company/company.module.ts +++ b/apps/api/src/modules/job-experience/company/company.module.ts @@ -1,5 +1,5 @@ -import { AuthorizationModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { CompanyService } from "@/modules/job-experience/company/company.service"; diff --git a/apps/server/src/modules/job-experience/company/company.policy.ts b/apps/api/src/modules/job-experience/company/company.policy.ts similarity index 80% rename from apps/server/src/modules/job-experience/company/company.policy.ts rename to apps/api/src/modules/job-experience/company/company.policy.ts index 4520273..1b8cdc1 100644 --- a/apps/server/src/modules/job-experience/company/company.policy.ts +++ b/apps/api/src/modules/job-experience/company/company.policy.ts @@ -1,4 +1,4 @@ -import { Policy, PublicResourcePolicy } from "@cv/auth"; +import { Policy, PublicResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Company } from "./company.entity"; diff --git a/apps/server/src/modules/job-experience/company/company.service.ts b/apps/api/src/modules/job-experience/company/company.service.ts similarity index 90% rename from apps/server/src/modules/job-experience/company/company.service.ts rename to apps/api/src/modules/job-experience/company/company.service.ts index 3828c89..7bbbdd4 100644 --- a/apps/server/src/modules/job-experience/company/company.service.ts +++ b/apps/api/src/modules/job-experience/company/company.service.ts @@ -1,4 +1,4 @@ -import { NamedEntityService, PrismaService } from "@cv/system"; +import { NamedEntityService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Company } from "@/modules/job-experience/company/company.entity"; import { CompanyMapper } from "./company.mapper"; diff --git a/apps/server/src/modules/job-experience/company/graphql/company-connection-args.type.ts b/apps/api/src/modules/job-experience/company/graphql/company-connection-args.type.ts similarity index 83% rename from apps/server/src/modules/job-experience/company/graphql/company-connection-args.type.ts rename to apps/api/src/modules/job-experience/company/graphql/company-connection-args.type.ts index b133c34..db8ebbe 100644 --- a/apps/server/src/modules/job-experience/company/graphql/company-connection-args.type.ts +++ b/apps/api/src/modules/job-experience/company/graphql/company-connection-args.type.ts @@ -1,4 +1,4 @@ -import { BasePaginationArgs } from "@cv/system"; +import { BasePaginationArgs } from "@cv/core"; import { ArgsType, Field } from "@nestjs/graphql"; import { GraphQLString } from "graphql"; diff --git a/apps/server/src/modules/job-experience/company/graphql/company.resolver.ts b/apps/api/src/modules/job-experience/company/graphql/company.resolver.ts similarity index 95% rename from apps/server/src/modules/job-experience/company/graphql/company.resolver.ts rename to apps/api/src/modules/job-experience/company/graphql/company.resolver.ts index 50a8d74..ceb729b 100644 --- a/apps/server/src/modules/job-experience/company/graphql/company.resolver.ts +++ b/apps/api/src/modules/job-experience/company/graphql/company.resolver.ts @@ -1,10 +1,7 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; -import { PaginationService } from "@cv/system"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; +import { PaginationService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; diff --git a/apps/server/src/modules/job-experience/company/graphql/company.type.ts b/apps/api/src/modules/job-experience/company/graphql/company.type.ts similarity index 100% rename from apps/server/src/modules/job-experience/company/graphql/company.type.ts rename to apps/api/src/modules/job-experience/company/graphql/company.type.ts diff --git a/apps/server/src/modules/job-experience/employment/employment.module.ts b/apps/api/src/modules/job-experience/employment/employment.module.ts similarity index 92% rename from apps/server/src/modules/job-experience/employment/employment.module.ts rename to apps/api/src/modules/job-experience/employment/employment.module.ts index 0a5cd8e..98d07fe 100644 --- a/apps/server/src/modules/job-experience/employment/employment.module.ts +++ b/apps/api/src/modules/job-experience/employment/employment.module.ts @@ -1,5 +1,5 @@ -import { AuthorizationModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { CompanyModule } from "@/modules/job-experience/company/company.module"; diff --git a/apps/server/src/modules/job-experience/employment/graphql/employment.resolver.ts b/apps/api/src/modules/job-experience/employment/graphql/employment.resolver.ts similarity index 97% rename from apps/server/src/modules/job-experience/employment/graphql/employment.resolver.ts rename to apps/api/src/modules/job-experience/employment/graphql/employment.resolver.ts index 094fef7..ce70aeb 100644 --- a/apps/server/src/modules/job-experience/employment/graphql/employment.resolver.ts +++ b/apps/api/src/modules/job-experience/employment/graphql/employment.resolver.ts @@ -1,9 +1,6 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Resolver } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; diff --git a/apps/server/src/modules/job-experience/employment/graphql/user-field.resolver.ts b/apps/api/src/modules/job-experience/employment/graphql/user-field.resolver.ts similarity index 99% rename from apps/server/src/modules/job-experience/employment/graphql/user-field.resolver.ts rename to apps/api/src/modules/job-experience/employment/graphql/user-field.resolver.ts index 6d2313f..2ef5d0d 100644 --- a/apps/server/src/modules/job-experience/employment/graphql/user-field.resolver.ts +++ b/apps/api/src/modules/job-experience/employment/graphql/user-field.resolver.ts @@ -1,5 +1,5 @@ import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; -import { PaginationArgs, PaginationService, PrismaService } from "@cv/system"; +import { PaginationArgs, PaginationService, PrismaService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Parent, ResolveField, Resolver } from "@nestjs/graphql"; import { UserJobExperienceService } from "@/modules/job-experience/employment/user-job-experience.service"; diff --git a/apps/server/src/modules/job-experience/employment/graphql/user-job-experience.type.ts b/apps/api/src/modules/job-experience/employment/graphql/user-job-experience.type.ts similarity index 100% rename from apps/server/src/modules/job-experience/employment/graphql/user-job-experience.type.ts rename to apps/api/src/modules/job-experience/employment/graphql/user-job-experience.type.ts diff --git a/apps/server/src/modules/job-experience/employment/onboarding/career-history.step.ts b/apps/api/src/modules/job-experience/employment/onboarding/career-history.step.ts similarity index 90% rename from apps/server/src/modules/job-experience/employment/onboarding/career-history.step.ts rename to apps/api/src/modules/job-experience/employment/onboarding/career-history.step.ts index 526b7c8..d4caeeb 100644 --- a/apps/server/src/modules/job-experience/employment/onboarding/career-history.step.ts +++ b/apps/api/src/modules/job-experience/employment/onboarding/career-history.step.ts @@ -1,5 +1,5 @@ -import type { User } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { OnboardingStep } from "@/modules/onboarding/onboarding-step.decorator"; import { diff --git a/apps/server/src/modules/job-experience/employment/user-job-experience.dto.ts b/apps/api/src/modules/job-experience/employment/user-job-experience.dto.ts similarity index 100% rename from apps/server/src/modules/job-experience/employment/user-job-experience.dto.ts rename to apps/api/src/modules/job-experience/employment/user-job-experience.dto.ts diff --git a/apps/server/src/modules/job-experience/employment/user-job-experience.entity.ts b/apps/api/src/modules/job-experience/employment/user-job-experience.entity.ts similarity index 96% rename from apps/server/src/modules/job-experience/employment/user-job-experience.entity.ts rename to apps/api/src/modules/job-experience/employment/user-job-experience.entity.ts index fb2523f..6d9bab3 100644 --- a/apps/server/src/modules/job-experience/employment/user-job-experience.entity.ts +++ b/apps/api/src/modules/job-experience/employment/user-job-experience.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; import type { Company } from "@/modules/job-experience/company/company.entity"; import type { Level } from "@/modules/job-experience/level/level.entity"; import type { Role } from "@/modules/job-experience/role/role.entity"; diff --git a/apps/server/src/modules/job-experience/employment/user-job-experience.mapper.ts b/apps/api/src/modules/job-experience/employment/user-job-experience.mapper.ts similarity index 98% rename from apps/server/src/modules/job-experience/employment/user-job-experience.mapper.ts rename to apps/api/src/modules/job-experience/employment/user-job-experience.mapper.ts index 219b7e4..e43ecda 100644 --- a/apps/server/src/modules/job-experience/employment/user-job-experience.mapper.ts +++ b/apps/api/src/modules/job-experience/employment/user-job-experience.mapper.ts @@ -1,4 +1,4 @@ -import type { BaseMapper } from "@cv/system"; +import type { BaseMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Company as PrismaCompany, diff --git a/apps/server/src/modules/job-experience/employment/user-job-experience.policy.ts b/apps/api/src/modules/job-experience/employment/user-job-experience.policy.ts similarity index 74% rename from apps/server/src/modules/job-experience/employment/user-job-experience.policy.ts rename to apps/api/src/modules/job-experience/employment/user-job-experience.policy.ts index f4e2fa2..7a037a6 100644 --- a/apps/server/src/modules/job-experience/employment/user-job-experience.policy.ts +++ b/apps/api/src/modules/job-experience/employment/user-job-experience.policy.ts @@ -1,5 +1,5 @@ -import { Policy, ProfileOwnedResourcePolicy } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import { Policy, ProfileOwnedResourcePolicy } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { UserJobExperience } from "./user-job-experience.entity"; diff --git a/apps/server/src/modules/job-experience/employment/user-job-experience.service.ts b/apps/api/src/modules/job-experience/employment/user-job-experience.service.ts similarity index 97% rename from apps/server/src/modules/job-experience/employment/user-job-experience.service.ts rename to apps/api/src/modules/job-experience/employment/user-job-experience.service.ts index 677b65f..d47263c 100644 --- a/apps/server/src/modules/job-experience/employment/user-job-experience.service.ts +++ b/apps/api/src/modules/job-experience/employment/user-job-experience.service.ts @@ -1,6 +1,6 @@ -import { notFound } from "@cv/auth"; -import type { PaginationOptions, PaginationResult } from "@cv/system"; -import { PaginationService, PrismaService } from "@cv/system"; +import { notFound } from "@cv/core"; +import type { PaginationOptions, PaginationResult } from "@cv/core"; +import { PaginationService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Prisma } from "@prisma/client"; import type { diff --git a/apps/server/src/modules/job-experience/level/graphql/level-connection-args.type.ts b/apps/api/src/modules/job-experience/level/graphql/level-connection-args.type.ts similarity index 83% rename from apps/server/src/modules/job-experience/level/graphql/level-connection-args.type.ts rename to apps/api/src/modules/job-experience/level/graphql/level-connection-args.type.ts index 606bc87..7573fef 100644 --- a/apps/server/src/modules/job-experience/level/graphql/level-connection-args.type.ts +++ b/apps/api/src/modules/job-experience/level/graphql/level-connection-args.type.ts @@ -1,4 +1,4 @@ -import { BasePaginationArgs } from "@cv/system"; +import { BasePaginationArgs } from "@cv/core"; import { ArgsType, Field } from "@nestjs/graphql"; import { GraphQLString } from "graphql"; diff --git a/apps/server/src/modules/job-experience/level/graphql/level.resolver.ts b/apps/api/src/modules/job-experience/level/graphql/level.resolver.ts similarity index 94% rename from apps/server/src/modules/job-experience/level/graphql/level.resolver.ts rename to apps/api/src/modules/job-experience/level/graphql/level.resolver.ts index d3f1f69..ab88a97 100644 --- a/apps/server/src/modules/job-experience/level/graphql/level.resolver.ts +++ b/apps/api/src/modules/job-experience/level/graphql/level.resolver.ts @@ -1,10 +1,7 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; -import { PaginationService } from "@cv/system"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; +import { PaginationService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; diff --git a/apps/server/src/modules/job-experience/level/graphql/level.type.ts b/apps/api/src/modules/job-experience/level/graphql/level.type.ts similarity index 100% rename from apps/server/src/modules/job-experience/level/graphql/level.type.ts rename to apps/api/src/modules/job-experience/level/graphql/level.type.ts diff --git a/apps/server/src/modules/job-experience/level/level.dataloader.ts b/apps/api/src/modules/job-experience/level/level.dataloader.ts similarity index 92% rename from apps/server/src/modules/job-experience/level/level.dataloader.ts rename to apps/api/src/modules/job-experience/level/level.dataloader.ts index 46bcd8e..96842fd 100644 --- a/apps/server/src/modules/job-experience/level/level.dataloader.ts +++ b/apps/api/src/modules/job-experience/level/level.dataloader.ts @@ -1,4 +1,4 @@ -import { BaseDataLoaderService } from "@cv/system"; +import { BaseDataLoaderService } from "@cv/core"; import { Injectable, Scope } from "@nestjs/common"; import { Level } from "./level.entity"; import { LevelService } from "./level.service"; diff --git a/apps/server/src/modules/job-experience/level/level.dto.ts b/apps/api/src/modules/job-experience/level/level.dto.ts similarity index 100% rename from apps/server/src/modules/job-experience/level/level.dto.ts rename to apps/api/src/modules/job-experience/level/level.dto.ts diff --git a/apps/server/src/modules/job-experience/level/level.entity.ts b/apps/api/src/modules/job-experience/level/level.entity.ts similarity index 50% rename from apps/server/src/modules/job-experience/level/level.entity.ts rename to apps/api/src/modules/job-experience/level/level.entity.ts index 331b4b4..11eeeb8 100644 --- a/apps/server/src/modules/job-experience/level/level.entity.ts +++ b/apps/api/src/modules/job-experience/level/level.entity.ts @@ -1,3 +1,3 @@ -import { NamedEntity } from "@cv/system"; +import { NamedEntity } from "@cv/core"; export class Level extends NamedEntity {} diff --git a/apps/server/src/modules/job-experience/level/level.factory.ts b/apps/api/src/modules/job-experience/level/level.factory.ts similarity index 88% rename from apps/server/src/modules/job-experience/level/level.factory.ts rename to apps/api/src/modules/job-experience/level/level.factory.ts index b21500f..b740a7d 100644 --- a/apps/server/src/modules/job-experience/level/level.factory.ts +++ b/apps/api/src/modules/job-experience/level/level.factory.ts @@ -1,4 +1,4 @@ -import { ClockService, Factory, UuidFactoryService } from "@cv/system"; +import { ClockService, Factory, UuidFactoryService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { CreateLevelDto } from "./level.dto"; import { Level } from "./level.entity"; diff --git a/apps/server/src/modules/job-experience/level/level.mapper.ts b/apps/api/src/modules/job-experience/level/level.mapper.ts similarity index 93% rename from apps/server/src/modules/job-experience/level/level.mapper.ts rename to apps/api/src/modules/job-experience/level/level.mapper.ts index 7d4bb25..6b2ae15 100644 --- a/apps/server/src/modules/job-experience/level/level.mapper.ts +++ b/apps/api/src/modules/job-experience/level/level.mapper.ts @@ -1,4 +1,4 @@ -import { createNamedEntityMapper } from "@cv/system"; +import { createNamedEntityMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Level as PrismaLevel } from "@prisma/client"; import { Level } from "@/modules/job-experience/level/level.entity"; diff --git a/apps/server/src/modules/job-experience/level/level.module.ts b/apps/api/src/modules/job-experience/level/level.module.ts similarity index 88% rename from apps/server/src/modules/job-experience/level/level.module.ts rename to apps/api/src/modules/job-experience/level/level.module.ts index 071a9ba..596f047 100644 --- a/apps/server/src/modules/job-experience/level/level.module.ts +++ b/apps/api/src/modules/job-experience/level/level.module.ts @@ -1,5 +1,5 @@ -import { AuthorizationModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { LevelService } from "@/modules/job-experience/level/level.service"; diff --git a/apps/server/src/modules/job-experience/level/level.policy.ts b/apps/api/src/modules/job-experience/level/level.policy.ts similarity index 80% rename from apps/server/src/modules/job-experience/level/level.policy.ts rename to apps/api/src/modules/job-experience/level/level.policy.ts index 4101686..c956ac9 100644 --- a/apps/server/src/modules/job-experience/level/level.policy.ts +++ b/apps/api/src/modules/job-experience/level/level.policy.ts @@ -1,4 +1,4 @@ -import { Policy, PublicResourcePolicy } from "@cv/auth"; +import { Policy, PublicResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Level } from "./level.entity"; diff --git a/apps/server/src/modules/job-experience/level/level.service.ts b/apps/api/src/modules/job-experience/level/level.service.ts similarity index 85% rename from apps/server/src/modules/job-experience/level/level.service.ts rename to apps/api/src/modules/job-experience/level/level.service.ts index 56dd311..2010294 100644 --- a/apps/server/src/modules/job-experience/level/level.service.ts +++ b/apps/api/src/modules/job-experience/level/level.service.ts @@ -1,4 +1,4 @@ -import { NamedEntityService, PrismaService } from "@cv/system"; +import { NamedEntityService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Level } from "@/modules/job-experience/level/level.entity"; import { LevelMapper } from "./level.mapper"; diff --git a/apps/server/src/modules/job-experience/role/graphql/role-connection-args.type.ts b/apps/api/src/modules/job-experience/role/graphql/role-connection-args.type.ts similarity index 83% rename from apps/server/src/modules/job-experience/role/graphql/role-connection-args.type.ts rename to apps/api/src/modules/job-experience/role/graphql/role-connection-args.type.ts index fee2439..479f7ec 100644 --- a/apps/server/src/modules/job-experience/role/graphql/role-connection-args.type.ts +++ b/apps/api/src/modules/job-experience/role/graphql/role-connection-args.type.ts @@ -1,4 +1,4 @@ -import { BasePaginationArgs } from "@cv/system"; +import { BasePaginationArgs } from "@cv/core"; import { ArgsType, Field } from "@nestjs/graphql"; import { GraphQLString } from "graphql"; diff --git a/apps/server/src/modules/job-experience/role/graphql/role.resolver.ts b/apps/api/src/modules/job-experience/role/graphql/role.resolver.ts similarity index 94% rename from apps/server/src/modules/job-experience/role/graphql/role.resolver.ts rename to apps/api/src/modules/job-experience/role/graphql/role.resolver.ts index 4eeeacb..09255c1 100644 --- a/apps/server/src/modules/job-experience/role/graphql/role.resolver.ts +++ b/apps/api/src/modules/job-experience/role/graphql/role.resolver.ts @@ -1,10 +1,7 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; -import { PaginationService } from "@cv/system"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; +import { PaginationService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; diff --git a/apps/server/src/modules/job-experience/role/graphql/role.type.ts b/apps/api/src/modules/job-experience/role/graphql/role.type.ts similarity index 100% rename from apps/server/src/modules/job-experience/role/graphql/role.type.ts rename to apps/api/src/modules/job-experience/role/graphql/role.type.ts diff --git a/apps/server/src/modules/job-experience/role/role.dataloader.ts b/apps/api/src/modules/job-experience/role/role.dataloader.ts similarity index 92% rename from apps/server/src/modules/job-experience/role/role.dataloader.ts rename to apps/api/src/modules/job-experience/role/role.dataloader.ts index 57c9a4b..666d538 100644 --- a/apps/server/src/modules/job-experience/role/role.dataloader.ts +++ b/apps/api/src/modules/job-experience/role/role.dataloader.ts @@ -1,4 +1,4 @@ -import { BaseDataLoaderService } from "@cv/system"; +import { BaseDataLoaderService } from "@cv/core"; import { Injectable, Scope } from "@nestjs/common"; import { Role } from "./role.entity"; import { RoleService } from "./role.service"; diff --git a/apps/server/src/modules/job-experience/role/role.dto.ts b/apps/api/src/modules/job-experience/role/role.dto.ts similarity index 100% rename from apps/server/src/modules/job-experience/role/role.dto.ts rename to apps/api/src/modules/job-experience/role/role.dto.ts diff --git a/apps/server/src/modules/job-experience/role/role.entity.ts b/apps/api/src/modules/job-experience/role/role.entity.ts similarity index 50% rename from apps/server/src/modules/job-experience/role/role.entity.ts rename to apps/api/src/modules/job-experience/role/role.entity.ts index 51ff3eb..ef61b70 100644 --- a/apps/server/src/modules/job-experience/role/role.entity.ts +++ b/apps/api/src/modules/job-experience/role/role.entity.ts @@ -1,3 +1,3 @@ -import { NamedEntity } from "@cv/system"; +import { NamedEntity } from "@cv/core"; export class Role extends NamedEntity {} diff --git a/apps/server/src/modules/job-experience/role/role.factory.ts b/apps/api/src/modules/job-experience/role/role.factory.ts similarity index 88% rename from apps/server/src/modules/job-experience/role/role.factory.ts rename to apps/api/src/modules/job-experience/role/role.factory.ts index a1a95f9..b9a9310 100644 --- a/apps/server/src/modules/job-experience/role/role.factory.ts +++ b/apps/api/src/modules/job-experience/role/role.factory.ts @@ -1,4 +1,4 @@ -import { ClockService, Factory, UuidFactoryService } from "@cv/system"; +import { ClockService, Factory, UuidFactoryService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { CreateRoleDto } from "./role.dto"; import { Role } from "./role.entity"; diff --git a/apps/server/src/modules/job-experience/role/role.mapper.ts b/apps/api/src/modules/job-experience/role/role.mapper.ts similarity index 93% rename from apps/server/src/modules/job-experience/role/role.mapper.ts rename to apps/api/src/modules/job-experience/role/role.mapper.ts index 6d53b80..f5eff4e 100644 --- a/apps/server/src/modules/job-experience/role/role.mapper.ts +++ b/apps/api/src/modules/job-experience/role/role.mapper.ts @@ -1,4 +1,4 @@ -import { createNamedEntityMapper } from "@cv/system"; +import { createNamedEntityMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Role as PrismaRole } from "@prisma/client"; import { Role } from "@/modules/job-experience/role/role.entity"; diff --git a/apps/server/src/modules/job-experience/role/role.module.ts b/apps/api/src/modules/job-experience/role/role.module.ts similarity index 88% rename from apps/server/src/modules/job-experience/role/role.module.ts rename to apps/api/src/modules/job-experience/role/role.module.ts index 75d825d..ecf55d5 100644 --- a/apps/server/src/modules/job-experience/role/role.module.ts +++ b/apps/api/src/modules/job-experience/role/role.module.ts @@ -1,5 +1,5 @@ -import { AuthorizationModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { RoleService } from "@/modules/job-experience/role/role.service"; diff --git a/apps/server/src/modules/job-experience/role/role.policy.ts b/apps/api/src/modules/job-experience/role/role.policy.ts similarity index 79% rename from apps/server/src/modules/job-experience/role/role.policy.ts rename to apps/api/src/modules/job-experience/role/role.policy.ts index ebd04a6..f761074 100644 --- a/apps/server/src/modules/job-experience/role/role.policy.ts +++ b/apps/api/src/modules/job-experience/role/role.policy.ts @@ -1,4 +1,4 @@ -import { Policy, PublicResourcePolicy } from "@cv/auth"; +import { Policy, PublicResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Role } from "./role.entity"; diff --git a/apps/server/src/modules/job-experience/role/role.service.ts b/apps/api/src/modules/job-experience/role/role.service.ts similarity index 84% rename from apps/server/src/modules/job-experience/role/role.service.ts rename to apps/api/src/modules/job-experience/role/role.service.ts index e16f8da..db20ee5 100644 --- a/apps/server/src/modules/job-experience/role/role.service.ts +++ b/apps/api/src/modules/job-experience/role/role.service.ts @@ -1,4 +1,4 @@ -import { NamedEntityService, PrismaService } from "@cv/system"; +import { NamedEntityService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Role } from "@/modules/job-experience/role/role.entity"; import { RoleMapper } from "./role.mapper"; diff --git a/apps/server/src/modules/job-experience/seed/job-experience.seed.ts b/apps/api/src/modules/job-experience/seed/job-experience.seed.ts similarity index 98% rename from apps/server/src/modules/job-experience/seed/job-experience.seed.ts rename to apps/api/src/modules/job-experience/seed/job-experience.seed.ts index cae99d3..839cc75 100644 --- a/apps/server/src/modules/job-experience/seed/job-experience.seed.ts +++ b/apps/api/src/modules/job-experience/seed/job-experience.seed.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { faker } from "@faker-js/faker"; import { Injectable, Logger } from "@nestjs/common"; import { Seeder } from "@/modules/database/seed/seed.service"; diff --git a/apps/server/src/modules/job-experience/seed/reference-data.seed.ts b/apps/api/src/modules/job-experience/seed/reference-data.seed.ts similarity index 80% rename from apps/server/src/modules/job-experience/seed/reference-data.seed.ts rename to apps/api/src/modules/job-experience/seed/reference-data.seed.ts index dc7983d..f6e568a 100644 --- a/apps/server/src/modules/job-experience/seed/reference-data.seed.ts +++ b/apps/api/src/modules/job-experience/seed/reference-data.seed.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { faker } from "@faker-js/faker"; import { Injectable, Logger } from "@nestjs/common"; import { Seeder } from "@/modules/database/seed/seed.service"; @@ -20,6 +20,8 @@ export class ReferenceDataSeedService implements Seeder { this.ensureLevels(prisma), this.ensureJobTypes(prisma), this.ensureApplicationStatuses(prisma), + this.ensureRateTypes(prisma), + this.ensureContractTypes(prisma), ]); } @@ -141,6 +143,36 @@ export class ReferenceDataSeedService implements Seeder { ); } + async ensureRateTypes( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = ["HOUR", "AGREEMENT", "FIXED", "DAY"]; + + return Promise.all( + names.map(async (name) => { + const existing = await prisma.rateType.findFirst({ where: { name } }); + if (existing) return { id: existing.id }; + const rt = await prisma.rateType.create({ data: { name } }); + return { id: rt.id }; + }), + ); + } + + async ensureContractTypes( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = ["FREELANCE", "FIXED_TERM", "PERMANENT", "INTERNSHIP"]; + + return Promise.all( + names.map(async (name) => { + const existing = await prisma.contractType.findFirst({ where: { name } }); + if (existing) return { id: existing.id }; + const ct = await prisma.contractType.create({ data: { name } }); + return { id: ct.id }; + }), + ); + } + async ensureApplicationStatuses( prisma: PrismaService = this.prisma, ): Promise> { diff --git a/apps/server/src/modules/job-experience/skill/graphql/skill-connection-args.type.ts b/apps/api/src/modules/job-experience/skill/graphql/skill-connection-args.type.ts similarity index 69% rename from apps/server/src/modules/job-experience/skill/graphql/skill-connection-args.type.ts rename to apps/api/src/modules/job-experience/skill/graphql/skill-connection-args.type.ts index 21a93c5..d9030c9 100644 --- a/apps/server/src/modules/job-experience/skill/graphql/skill-connection-args.type.ts +++ b/apps/api/src/modules/job-experience/skill/graphql/skill-connection-args.type.ts @@ -1,4 +1,4 @@ -import { SearchablePaginationArgs } from "@cv/system"; +import { SearchablePaginationArgs } from "@cv/core"; import { ArgsType } from "@nestjs/graphql"; @ArgsType() diff --git a/apps/server/src/modules/job-experience/skill/graphql/skill.resolver.ts b/apps/api/src/modules/job-experience/skill/graphql/skill.resolver.ts similarity index 94% rename from apps/server/src/modules/job-experience/skill/graphql/skill.resolver.ts rename to apps/api/src/modules/job-experience/skill/graphql/skill.resolver.ts index 2750619..08678d6 100644 --- a/apps/server/src/modules/job-experience/skill/graphql/skill.resolver.ts +++ b/apps/api/src/modules/job-experience/skill/graphql/skill.resolver.ts @@ -1,10 +1,7 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; -import { PaginationService } from "@cv/system"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; +import { PaginationService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; diff --git a/apps/server/src/modules/job-experience/skill/graphql/skill.type.ts b/apps/api/src/modules/job-experience/skill/graphql/skill.type.ts similarity index 100% rename from apps/server/src/modules/job-experience/skill/graphql/skill.type.ts rename to apps/api/src/modules/job-experience/skill/graphql/skill.type.ts diff --git a/apps/server/src/modules/job-experience/skill/skill.dto.ts b/apps/api/src/modules/job-experience/skill/skill.dto.ts similarity index 100% rename from apps/server/src/modules/job-experience/skill/skill.dto.ts rename to apps/api/src/modules/job-experience/skill/skill.dto.ts diff --git a/apps/server/src/modules/job-experience/skill/skill.entity.ts b/apps/api/src/modules/job-experience/skill/skill.entity.ts similarity index 50% rename from apps/server/src/modules/job-experience/skill/skill.entity.ts rename to apps/api/src/modules/job-experience/skill/skill.entity.ts index 47c7f29..e53d8f9 100644 --- a/apps/server/src/modules/job-experience/skill/skill.entity.ts +++ b/apps/api/src/modules/job-experience/skill/skill.entity.ts @@ -1,3 +1,3 @@ -import { NamedEntity } from "@cv/system"; +import { NamedEntity } from "@cv/core"; export class Skill extends NamedEntity {} diff --git a/apps/server/src/modules/job-experience/skill/skill.factory.ts b/apps/api/src/modules/job-experience/skill/skill.factory.ts similarity index 88% rename from apps/server/src/modules/job-experience/skill/skill.factory.ts rename to apps/api/src/modules/job-experience/skill/skill.factory.ts index 7efb996..8015db6 100644 --- a/apps/server/src/modules/job-experience/skill/skill.factory.ts +++ b/apps/api/src/modules/job-experience/skill/skill.factory.ts @@ -1,4 +1,4 @@ -import { ClockService, Factory, UuidFactoryService } from "@cv/system"; +import { ClockService, Factory, UuidFactoryService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { CreateSkillDto } from "./skill.dto"; import { Skill } from "./skill.entity"; diff --git a/apps/server/src/modules/job-experience/skill/skill.mapper.ts b/apps/api/src/modules/job-experience/skill/skill.mapper.ts similarity index 93% rename from apps/server/src/modules/job-experience/skill/skill.mapper.ts rename to apps/api/src/modules/job-experience/skill/skill.mapper.ts index 47f3463..c2c2428 100644 --- a/apps/server/src/modules/job-experience/skill/skill.mapper.ts +++ b/apps/api/src/modules/job-experience/skill/skill.mapper.ts @@ -1,4 +1,4 @@ -import { createNamedEntityMapper } from "@cv/system"; +import { createNamedEntityMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Skill as PrismaSkill } from "@prisma/client"; import { Skill } from "@/modules/job-experience/skill/skill.entity"; diff --git a/apps/server/src/modules/job-experience/skill/skill.module.ts b/apps/api/src/modules/job-experience/skill/skill.module.ts similarity index 89% rename from apps/server/src/modules/job-experience/skill/skill.module.ts rename to apps/api/src/modules/job-experience/skill/skill.module.ts index b23ff33..c6783f4 100644 --- a/apps/server/src/modules/job-experience/skill/skill.module.ts +++ b/apps/api/src/modules/job-experience/skill/skill.module.ts @@ -1,5 +1,5 @@ -import { AuthorizationModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { SkillService } from "@/modules/job-experience/skill/skill.service"; diff --git a/apps/server/src/modules/job-experience/skill/skill.policy.ts b/apps/api/src/modules/job-experience/skill/skill.policy.ts similarity index 80% rename from apps/server/src/modules/job-experience/skill/skill.policy.ts rename to apps/api/src/modules/job-experience/skill/skill.policy.ts index b9f77f0..8892702 100644 --- a/apps/server/src/modules/job-experience/skill/skill.policy.ts +++ b/apps/api/src/modules/job-experience/skill/skill.policy.ts @@ -1,4 +1,4 @@ -import { Policy, PublicResourcePolicy } from "@cv/auth"; +import { Policy, PublicResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Skill } from "./skill.entity"; diff --git a/apps/server/src/modules/job-experience/skill/skill.service.ts b/apps/api/src/modules/job-experience/skill/skill.service.ts similarity index 97% rename from apps/server/src/modules/job-experience/skill/skill.service.ts rename to apps/api/src/modules/job-experience/skill/skill.service.ts index 5918cb9..a48c69a 100644 --- a/apps/server/src/modules/job-experience/skill/skill.service.ts +++ b/apps/api/src/modules/job-experience/skill/skill.service.ts @@ -2,7 +2,7 @@ import { type NamedEntityFilters, NamedEntityService, PrismaService, -} from "@cv/system"; +} from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Skill } from "@/modules/job-experience/skill/skill.entity"; import { SkillMapper } from "./skill.mapper"; diff --git a/apps/server/src/modules/messenger/logger.provider.ts b/apps/api/src/modules/messenger/logger.provider.ts similarity index 100% rename from apps/server/src/modules/messenger/logger.provider.ts rename to apps/api/src/modules/messenger/logger.provider.ts diff --git a/apps/server/src/modules/messenger/messages/render-pdf.message.ts b/apps/api/src/modules/messenger/messages/render-pdf.message.ts similarity index 100% rename from apps/server/src/modules/messenger/messages/render-pdf.message.ts rename to apps/api/src/modules/messenger/messages/render-pdf.message.ts diff --git a/apps/server/src/modules/messenger/messenger.module.ts b/apps/api/src/modules/messenger/messenger.module.ts similarity index 100% rename from apps/server/src/modules/messenger/messenger.module.ts rename to apps/api/src/modules/messenger/messenger.module.ts diff --git a/apps/server/src/modules/messenger/prisma-transport.module.ts b/apps/api/src/modules/messenger/prisma-transport.module.ts similarity index 85% rename from apps/server/src/modules/messenger/prisma-transport.module.ts rename to apps/api/src/modules/messenger/prisma-transport.module.ts index 24aa4b2..61f7c48 100644 --- a/apps/server/src/modules/messenger/prisma-transport.module.ts +++ b/apps/api/src/modules/messenger/prisma-transport.module.ts @@ -1,5 +1,5 @@ import { Module } from "@nestjs/common"; -import { DatabaseModule, PrismaService } from "@cv/system"; +import { DatabaseModule, PrismaService } from "@cv/core"; import { PrismaClientToken, PrismaTransportFactory } from "@riotbyte-com/project-q-prisma"; @Module({ diff --git a/apps/server/src/modules/onboarding/graphql/onboarding.resolver.ts b/apps/api/src/modules/onboarding/graphql/onboarding.resolver.ts similarity index 94% rename from apps/server/src/modules/onboarding/graphql/onboarding.resolver.ts rename to apps/api/src/modules/onboarding/graphql/onboarding.resolver.ts index 3723d44..61330cc 100644 --- a/apps/server/src/modules/onboarding/graphql/onboarding.resolver.ts +++ b/apps/api/src/modules/onboarding/graphql/onboarding.resolver.ts @@ -1,4 +1,4 @@ -import type { User as DomainUser } from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Mutation, Query, Resolver } from "@nestjs/graphql"; diff --git a/apps/server/src/modules/onboarding/graphql/onboarding.type.ts b/apps/api/src/modules/onboarding/graphql/onboarding.type.ts similarity index 100% rename from apps/server/src/modules/onboarding/graphql/onboarding.type.ts rename to apps/api/src/modules/onboarding/graphql/onboarding.type.ts diff --git a/apps/server/src/modules/onboarding/onboarding-step.decorator.ts b/apps/api/src/modules/onboarding/onboarding-step.decorator.ts similarity index 100% rename from apps/server/src/modules/onboarding/onboarding-step.decorator.ts rename to apps/api/src/modules/onboarding/onboarding-step.decorator.ts diff --git a/apps/server/src/modules/onboarding/onboarding-step.interface.ts b/apps/api/src/modules/onboarding/onboarding-step.interface.ts similarity index 88% rename from apps/server/src/modules/onboarding/onboarding-step.interface.ts rename to apps/api/src/modules/onboarding/onboarding-step.interface.ts index d79388a..7969535 100644 --- a/apps/server/src/modules/onboarding/onboarding-step.interface.ts +++ b/apps/api/src/modules/onboarding/onboarding-step.interface.ts @@ -1,4 +1,4 @@ -import type { User } from "@cv/auth"; +import type { User } from "@cv/core"; export enum OnboardingStepStatus { NOT_STARTED = "NOT_STARTED", diff --git a/apps/server/src/modules/onboarding/onboarding.module.ts b/apps/api/src/modules/onboarding/onboarding.module.ts similarity index 89% rename from apps/server/src/modules/onboarding/onboarding.module.ts rename to apps/api/src/modules/onboarding/onboarding.module.ts index b6d52d3..4afdd3a 100644 --- a/apps/server/src/modules/onboarding/onboarding.module.ts +++ b/apps/api/src/modules/onboarding/onboarding.module.ts @@ -1,4 +1,4 @@ -import { DatabaseModule } from "@cv/system"; +import { DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { DiscoveryModule } from "@nestjs/core"; import { OnboardingResolver } from "./graphql/onboarding.resolver"; diff --git a/apps/server/src/modules/onboarding/onboarding.service.ts b/apps/api/src/modules/onboarding/onboarding.service.ts similarity index 97% rename from apps/server/src/modules/onboarding/onboarding.service.ts rename to apps/api/src/modules/onboarding/onboarding.service.ts index ca814f9..2f0cfd9 100644 --- a/apps/server/src/modules/onboarding/onboarding.service.ts +++ b/apps/api/src/modules/onboarding/onboarding.service.ts @@ -1,5 +1,5 @@ -import type { User } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; import { DiscoveryService } from "@nestjs/core"; import { ONBOARDING_STEP_METADATA_KEY } from "./onboarding-step.decorator"; diff --git a/apps/server/src/modules/organization/graphql/membership-connection-args.type.ts b/apps/api/src/modules/organization/graphql/membership-connection-args.type.ts similarity index 82% rename from apps/server/src/modules/organization/graphql/membership-connection-args.type.ts rename to apps/api/src/modules/organization/graphql/membership-connection-args.type.ts index 300f5a0..17a9e57 100644 --- a/apps/server/src/modules/organization/graphql/membership-connection-args.type.ts +++ b/apps/api/src/modules/organization/graphql/membership-connection-args.type.ts @@ -1,4 +1,4 @@ -import { SortablePaginationArgs } from "@cv/system"; +import { SortablePaginationArgs } from "@cv/core"; import { ArgsType, Field } from "@nestjs/graphql"; @ArgsType() diff --git a/apps/server/src/modules/organization/graphql/membership.type.ts b/apps/api/src/modules/organization/graphql/membership.type.ts similarity index 98% rename from apps/server/src/modules/organization/graphql/membership.type.ts rename to apps/api/src/modules/organization/graphql/membership.type.ts index 6eed774..41ec9e7 100644 --- a/apps/server/src/modules/organization/graphql/membership.type.ts +++ b/apps/api/src/modules/organization/graphql/membership.type.ts @@ -1,4 +1,4 @@ -import { UserRole } from "@cv/auth"; +import { UserRole } from "@cv/core"; import { Field, ObjectType } from "@nestjs/graphql"; import { createConnection } from "@/modules/base/connection.factory"; import { OrganizationRole } from "@/modules/organization/organization-role.entity"; diff --git a/apps/server/src/modules/organization/graphql/organization.resolver.ts b/apps/api/src/modules/organization/graphql/organization.resolver.ts similarity index 96% rename from apps/server/src/modules/organization/graphql/organization.resolver.ts rename to apps/api/src/modules/organization/graphql/organization.resolver.ts index 852d08f..e66b053 100644 --- a/apps/server/src/modules/organization/graphql/organization.resolver.ts +++ b/apps/api/src/modules/organization/graphql/organization.resolver.ts @@ -1,11 +1,7 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - UserService, - VerifiedScopeGuard, -} from "@cv/auth"; -import { PaginationService } from "@cv/system"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService, UserService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; +import { PaginationService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, diff --git a/apps/server/src/modules/organization/graphql/user-field.resolver.ts b/apps/api/src/modules/organization/graphql/user-field.resolver.ts similarity index 100% rename from apps/server/src/modules/organization/graphql/user-field.resolver.ts rename to apps/api/src/modules/organization/graphql/user-field.resolver.ts diff --git a/apps/server/src/modules/organization/organization-role.entity.ts b/apps/api/src/modules/organization/organization-role.entity.ts similarity index 96% rename from apps/server/src/modules/organization/organization-role.entity.ts rename to apps/api/src/modules/organization/organization-role.entity.ts index fc523e0..44930b9 100644 --- a/apps/server/src/modules/organization/organization-role.entity.ts +++ b/apps/api/src/modules/organization/organization-role.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; import { Field, ID, ObjectType } from "@nestjs/graphql"; @ObjectType() diff --git a/apps/server/src/modules/organization/organization-role.mapper.ts b/apps/api/src/modules/organization/organization-role.mapper.ts similarity index 95% rename from apps/server/src/modules/organization/organization-role.mapper.ts rename to apps/api/src/modules/organization/organization-role.mapper.ts index 6a3130d..77b1e09 100644 --- a/apps/server/src/modules/organization/organization-role.mapper.ts +++ b/apps/api/src/modules/organization/organization-role.mapper.ts @@ -1,4 +1,4 @@ -import type { BaseMapper } from "@cv/system"; +import type { BaseMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { OrganizationRole as PrismaOrganizationRole } from "@prisma/client"; import { OrganizationRole } from "./organization-role.entity"; diff --git a/apps/server/src/modules/organization/organization-role.policy.ts b/apps/api/src/modules/organization/organization-role.policy.ts similarity index 80% rename from apps/server/src/modules/organization/organization-role.policy.ts rename to apps/api/src/modules/organization/organization-role.policy.ts index 3e8f726..200480c 100644 --- a/apps/server/src/modules/organization/organization-role.policy.ts +++ b/apps/api/src/modules/organization/organization-role.policy.ts @@ -1,4 +1,4 @@ -import { Policy, PublicResourcePolicy } from "@cv/auth"; +import { Policy, PublicResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { OrganizationRole } from "./organization-role.entity"; diff --git a/apps/server/src/modules/organization/organization-role.service.ts b/apps/api/src/modules/organization/organization-role.service.ts similarity index 96% rename from apps/server/src/modules/organization/organization-role.service.ts rename to apps/api/src/modules/organization/organization-role.service.ts index 2d45e94..769f4f6 100644 --- a/apps/server/src/modules/organization/organization-role.service.ts +++ b/apps/api/src/modules/organization/organization-role.service.ts @@ -1,5 +1,5 @@ -import { notFound } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import { notFound } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { OrganizationRole } from "./organization-role.entity"; import { OrganizationRoleMapper } from "./organization-role.mapper"; diff --git a/apps/server/src/modules/organization/organization.dto.ts b/apps/api/src/modules/organization/organization.dto.ts similarity index 100% rename from apps/server/src/modules/organization/organization.dto.ts rename to apps/api/src/modules/organization/organization.dto.ts diff --git a/apps/server/src/modules/organization/organization.entity.ts b/apps/api/src/modules/organization/organization.entity.ts similarity index 95% rename from apps/server/src/modules/organization/organization.entity.ts rename to apps/api/src/modules/organization/organization.entity.ts index 927a1f1..bcbbfb0 100644 --- a/apps/server/src/modules/organization/organization.entity.ts +++ b/apps/api/src/modules/organization/organization.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; import { Field, ID, ObjectType } from "@nestjs/graphql"; @ObjectType() diff --git a/apps/server/src/modules/organization/organization.factory.ts b/apps/api/src/modules/organization/organization.factory.ts similarity index 89% rename from apps/server/src/modules/organization/organization.factory.ts rename to apps/api/src/modules/organization/organization.factory.ts index a1b03f4..7a4cc02 100644 --- a/apps/server/src/modules/organization/organization.factory.ts +++ b/apps/api/src/modules/organization/organization.factory.ts @@ -1,4 +1,4 @@ -import { ClockService, Factory, UuidFactoryService } from "@cv/system"; +import { ClockService, Factory, UuidFactoryService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { CreateOrganizationDto } from "./organization.dto"; import { Organization } from "./organization.entity"; diff --git a/apps/server/src/modules/organization/organization.mapper.ts b/apps/api/src/modules/organization/organization.mapper.ts similarity index 95% rename from apps/server/src/modules/organization/organization.mapper.ts rename to apps/api/src/modules/organization/organization.mapper.ts index d0b22b0..7f02093 100644 --- a/apps/server/src/modules/organization/organization.mapper.ts +++ b/apps/api/src/modules/organization/organization.mapper.ts @@ -1,4 +1,4 @@ -import type { BaseMapper } from "@cv/system"; +import type { BaseMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Organization as PrismaOrganization } from "@prisma/client"; import { Organization } from "./organization.entity"; diff --git a/apps/server/src/modules/organization/organization.module.ts b/apps/api/src/modules/organization/organization.module.ts similarity index 93% rename from apps/server/src/modules/organization/organization.module.ts rename to apps/api/src/modules/organization/organization.module.ts index d99afd3..02352c0 100644 --- a/apps/server/src/modules/organization/organization.module.ts +++ b/apps/api/src/modules/organization/organization.module.ts @@ -1,5 +1,5 @@ -import { AuthorizationModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { OrganizationResolver } from "./graphql/organization.resolver"; diff --git a/apps/server/src/modules/organization/organization.policy.ts b/apps/api/src/modules/organization/organization.policy.ts similarity index 82% rename from apps/server/src/modules/organization/organization.policy.ts rename to apps/api/src/modules/organization/organization.policy.ts index cdb1a85..e686bff 100644 --- a/apps/server/src/modules/organization/organization.policy.ts +++ b/apps/api/src/modules/organization/organization.policy.ts @@ -1,4 +1,4 @@ -import { Policy, PublicResourcePolicy } from "@cv/auth"; +import { Policy, PublicResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Organization } from "./organization.entity"; diff --git a/apps/server/src/modules/organization/organization.service.ts b/apps/api/src/modules/organization/organization.service.ts similarity index 97% rename from apps/server/src/modules/organization/organization.service.ts rename to apps/api/src/modules/organization/organization.service.ts index 1825c3f..968d9a0 100644 --- a/apps/server/src/modules/organization/organization.service.ts +++ b/apps/api/src/modules/organization/organization.service.ts @@ -1,5 +1,5 @@ -import { notFound, User } from "@cv/auth"; -import { type EntityService, PrismaService } from "@cv/system"; +import { notFound, User } from "@cv/core"; +import { type EntityService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { Prisma } from "@prisma/client"; import { Organization } from "./organization.entity"; diff --git a/apps/server/src/modules/organization/seed/membership.seed.ts b/apps/api/src/modules/organization/seed/membership.seed.ts similarity index 98% rename from apps/server/src/modules/organization/seed/membership.seed.ts rename to apps/api/src/modules/organization/seed/membership.seed.ts index 335fad1..39dc260 100644 --- a/apps/server/src/modules/organization/seed/membership.seed.ts +++ b/apps/api/src/modules/organization/seed/membership.seed.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { faker } from "@faker-js/faker"; import { Injectable, Logger } from "@nestjs/common"; import { Seeder } from "@/modules/database/seed/seed.service"; diff --git a/apps/server/src/modules/organization/seed/organization.seed.ts b/apps/api/src/modules/organization/seed/organization.seed.ts similarity index 97% rename from apps/server/src/modules/organization/seed/organization.seed.ts rename to apps/api/src/modules/organization/seed/organization.seed.ts index b3843e0..9e2f65e 100644 --- a/apps/server/src/modules/organization/seed/organization.seed.ts +++ b/apps/api/src/modules/organization/seed/organization.seed.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { faker } from "@faker-js/faker"; import { Injectable, Logger } from "@nestjs/common"; import { Seeder } from "@/modules/database/seed/seed.service"; diff --git a/apps/server/src/modules/profile/graphql/profile-field.resolver.ts b/apps/api/src/modules/profile/graphql/profile-field.resolver.ts similarity index 97% rename from apps/server/src/modules/profile/graphql/profile-field.resolver.ts rename to apps/api/src/modules/profile/graphql/profile-field.resolver.ts index 3eccaad..8ff070c 100644 --- a/apps/server/src/modules/profile/graphql/profile-field.resolver.ts +++ b/apps/api/src/modules/profile/graphql/profile-field.resolver.ts @@ -1,5 +1,5 @@ import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; -import { PaginationArgs, PaginationService } from "@cv/system"; +import { PaginationArgs, PaginationService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Parent, ResolveField, Resolver } from "@nestjs/graphql"; import { CVService } from "@/modules/cv-template/cv.service"; diff --git a/apps/server/src/modules/profile/graphql/profile.resolver.ts b/apps/api/src/modules/profile/graphql/profile.resolver.ts similarity index 97% rename from apps/server/src/modules/profile/graphql/profile.resolver.ts rename to apps/api/src/modules/profile/graphql/profile.resolver.ts index 6b8ccb3..f916287 100644 --- a/apps/server/src/modules/profile/graphql/profile.resolver.ts +++ b/apps/api/src/modules/profile/graphql/profile.resolver.ts @@ -1,4 +1,4 @@ -import type { User as DomainUser } from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Args, ID, Mutation, Query, Resolver } from "@nestjs/graphql"; diff --git a/apps/server/src/modules/profile/graphql/profile.type.ts b/apps/api/src/modules/profile/graphql/profile.type.ts similarity index 87% rename from apps/server/src/modules/profile/graphql/profile.type.ts rename to apps/api/src/modules/profile/graphql/profile.type.ts index c36500e..6b7650b 100644 --- a/apps/server/src/modules/profile/graphql/profile.type.ts +++ b/apps/api/src/modules/profile/graphql/profile.type.ts @@ -23,11 +23,7 @@ export class ProfileType { @Field(() => String, { nullable: true }) postalCode!: string | null; - @Field(() => String, { nullable: true }) - city!: string | null; - - @Field(() => String, { nullable: true }) - country!: string | null; + locationId!: string | null; @Field(() => String, { nullable: true }) website!: string | null; @@ -66,10 +62,7 @@ export class CreateProfileInput { postalCode?: string; @Field(() => String, { nullable: true }) - city?: string; - - @Field(() => String, { nullable: true }) - country?: string; + locationId?: string; @Field(() => String, { nullable: true }) website?: string; @@ -102,10 +95,7 @@ export class UpdateProfileInput { postalCode?: string; @Field(() => String, { nullable: true }) - city?: string; - - @Field(() => String, { nullable: true }) - country?: string; + locationId?: string; @Field(() => String, { nullable: true }) website?: string; diff --git a/apps/server/src/modules/profile/onboarding/profile.step.ts b/apps/api/src/modules/profile/onboarding/profile.step.ts similarity index 81% rename from apps/server/src/modules/profile/onboarding/profile.step.ts rename to apps/api/src/modules/profile/onboarding/profile.step.ts index 611b064..31a32c5 100644 --- a/apps/server/src/modules/profile/onboarding/profile.step.ts +++ b/apps/api/src/modules/profile/onboarding/profile.step.ts @@ -1,5 +1,5 @@ -import type { User } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { OnboardingStep } from "@/modules/onboarding/onboarding-step.decorator"; import { @@ -19,12 +19,12 @@ export class ProfileOnboardingStep implements OnboardingStepDefinition { async computeStatus(user: User): Promise { const profile = await this.prisma.profile.findFirst({ where: { userId: user.id }, - select: { headline: true, city: true }, + select: { headline: true, locationId: true }, }); if (!profile) return OnboardingStepStatus.NOT_STARTED; - return profile.headline && profile.city + return profile.headline && profile.locationId ? OnboardingStepStatus.COMPLETE : OnboardingStepStatus.IN_PROGRESS; } diff --git a/apps/server/src/modules/profile/profile.entity.ts b/apps/api/src/modules/profile/profile.entity.ts similarity index 72% rename from apps/server/src/modules/profile/profile.entity.ts rename to apps/api/src/modules/profile/profile.entity.ts index 12e0b24..d5f7724 100644 --- a/apps/server/src/modules/profile/profile.entity.ts +++ b/apps/api/src/modules/profile/profile.entity.ts @@ -1,10 +1,11 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; export class ProfileEntity extends BaseEntity { constructor( id: string, public readonly userId: string, public readonly name: string, + public readonly locationId: string | undefined, createdAt: Date, updatedAt: Date, ) { diff --git a/apps/server/src/modules/profile/profile.module.ts b/apps/api/src/modules/profile/profile.module.ts similarity index 94% rename from apps/server/src/modules/profile/profile.module.ts rename to apps/api/src/modules/profile/profile.module.ts index e2915fa..da43346 100644 --- a/apps/server/src/modules/profile/profile.module.ts +++ b/apps/api/src/modules/profile/profile.module.ts @@ -1,5 +1,5 @@ import { AuthModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module, forwardRef } from "@nestjs/common"; import { CVTemplateModule } from "@/modules/cv-template/cv-template.module"; import { EducationModule } from "@/modules/education/education.module"; diff --git a/apps/server/src/modules/profile/profile.policy.ts b/apps/api/src/modules/profile/profile.policy.ts similarity index 77% rename from apps/server/src/modules/profile/profile.policy.ts rename to apps/api/src/modules/profile/profile.policy.ts index 3d49bca..2670a40 100644 --- a/apps/server/src/modules/profile/profile.policy.ts +++ b/apps/api/src/modules/profile/profile.policy.ts @@ -1,4 +1,4 @@ -import { Policy, UserOwnedResourcePolicy } from "@cv/auth"; +import { Policy, UserOwnedResourcePolicy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { ProfileEntity } from "./profile.entity"; diff --git a/apps/server/src/modules/profile/profile.service.ts b/apps/api/src/modules/profile/profile.service.ts similarity index 90% rename from apps/server/src/modules/profile/profile.service.ts rename to apps/api/src/modules/profile/profile.service.ts index 86c7e3f..bca9fb2 100644 --- a/apps/server/src/modules/profile/profile.service.ts +++ b/apps/api/src/modules/profile/profile.service.ts @@ -1,6 +1,6 @@ -import type { User } from "@cv/auth"; -import { TokenEncryptionService } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { TokenEncryptionService } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable, NotFoundException } from "@nestjs/common"; interface CreateProfileInput { @@ -10,8 +10,7 @@ interface CreateProfileInput { phone?: string; address?: string; postalCode?: string; - city?: string; - country?: string; + locationId?: string; website?: string; linkedInUrl?: string; summary?: string; @@ -24,8 +23,7 @@ interface UpdateProfileInput { phone?: string; address?: string; postalCode?: string; - city?: string; - country?: string; + locationId?: string; website?: string; linkedInUrl?: string; summary?: string; @@ -40,8 +38,7 @@ export interface DecryptedProfile { phone: string | null; address: string | null; postalCode: string | null; - city: string | null; - country: string | null; + locationId: string | null; website: string | null; linkedInUrl: string | null; summary: string | null; @@ -144,10 +141,10 @@ export class ProfileService { async hasKeyFields(userId: string): Promise { const profile = await this.prisma.profile.findFirst({ where: { userId }, - select: { headline: true, city: true }, + select: { headline: true, locationId: true }, }); - return profile !== null && profile.headline !== null && profile.city !== null; + return profile !== null && profile.headline !== null && profile.locationId !== null; } private decrypt(profile: { @@ -159,8 +156,7 @@ export class ProfileService { encryptedPhone: string | null; encryptedAddress: string | null; encryptedPostalCode: string | null; - city: string | null; - country: string | null; + locationId: string | null; website: string | null; linkedInUrl: string | null; summary: string | null; @@ -182,8 +178,7 @@ export class ProfileService { postalCode: profile.encryptedPostalCode ? this.encryption.decrypt(profile.encryptedPostalCode) : null, - city: profile.city, - country: profile.country, + locationId: profile.locationId, website: profile.website, linkedInUrl: profile.linkedInUrl, summary: profile.summary, @@ -199,8 +194,8 @@ export class ProfileService { if (data.fullName !== undefined) result["fullName"] = data.fullName || null; if (data.headline !== undefined) result["headline"] = data.headline || null; - if (data.city !== undefined) result["city"] = data.city || null; - if (data.country !== undefined) result["country"] = data.country || null; + if (data.locationId !== undefined) + result["locationId"] = data.locationId || null; if (data.website !== undefined) result["website"] = data.website || null; if (data.linkedInUrl !== undefined) result["linkedInUrl"] = data.linkedInUrl || null; diff --git a/apps/server/src/modules/user-settings/onboarding/ai-preference.step.ts b/apps/api/src/modules/user-settings/onboarding/ai-preference.step.ts similarity index 90% rename from apps/server/src/modules/user-settings/onboarding/ai-preference.step.ts rename to apps/api/src/modules/user-settings/onboarding/ai-preference.step.ts index acc48d7..fe60d5c 100644 --- a/apps/server/src/modules/user-settings/onboarding/ai-preference.step.ts +++ b/apps/api/src/modules/user-settings/onboarding/ai-preference.step.ts @@ -1,5 +1,5 @@ -import type { User } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { OnboardingStep } from "@/modules/onboarding/onboarding-step.decorator"; import { diff --git a/apps/server/src/modules/user-settings/user-ai-settings.module.ts b/apps/api/src/modules/user-settings/user-ai-settings.module.ts similarity index 93% rename from apps/server/src/modules/user-settings/user-ai-settings.module.ts rename to apps/api/src/modules/user-settings/user-ai-settings.module.ts index d44da77..97a6467 100644 --- a/apps/server/src/modules/user-settings/user-ai-settings.module.ts +++ b/apps/api/src/modules/user-settings/user-ai-settings.module.ts @@ -1,5 +1,5 @@ import { AIModule, type AIProviderType } from "@cv/ai-provider"; -import { DatabaseModule } from "@cv/system"; +import { DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AiPreferenceOnboardingStep } from "./onboarding/ai-preference.step"; import { UserAiSettingsResolver } from "./user-ai-settings.resolver"; diff --git a/apps/server/src/modules/user-settings/user-ai-settings.resolver.ts b/apps/api/src/modules/user-settings/user-ai-settings.resolver.ts similarity index 99% rename from apps/server/src/modules/user-settings/user-ai-settings.resolver.ts rename to apps/api/src/modules/user-settings/user-ai-settings.resolver.ts index 72139a4..63460a8 100644 --- a/apps/server/src/modules/user-settings/user-ai-settings.resolver.ts +++ b/apps/api/src/modules/user-settings/user-ai-settings.resolver.ts @@ -1,6 +1,6 @@ import type { AIProvider } from "@cv/ai-provider"; import { AI_PROVIDER, registeredProviderTypes } from "@cv/ai-provider"; -import type { User as DomainUser } from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { Inject, Optional, UseGuards } from "@nestjs/common"; import { Args, Mutation, Query, Resolver } from "@nestjs/graphql"; diff --git a/apps/server/src/modules/user-settings/user-ai-settings.service.ts b/apps/api/src/modules/user-settings/user-ai-settings.service.ts similarity index 97% rename from apps/server/src/modules/user-settings/user-ai-settings.service.ts rename to apps/api/src/modules/user-settings/user-ai-settings.service.ts index 13eaf20..1e0decb 100644 --- a/apps/server/src/modules/user-settings/user-ai-settings.service.ts +++ b/apps/api/src/modules/user-settings/user-ai-settings.service.ts @@ -1,6 +1,6 @@ -import type { User } from "@cv/auth"; -import { TokenEncryptionService } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import type { User } from "@cv/core"; +import { TokenEncryptionService } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { BadRequestException, Injectable, diff --git a/apps/server/src/modules/user-settings/user-ai-settings.type.ts b/apps/api/src/modules/user-settings/user-ai-settings.type.ts similarity index 100% rename from apps/server/src/modules/user-settings/user-ai-settings.type.ts rename to apps/api/src/modules/user-settings/user-ai-settings.type.ts diff --git a/apps/server/src/modules/user/seed/user.seed.ts b/apps/api/src/modules/user/seed/user.seed.ts similarity index 97% rename from apps/server/src/modules/user/seed/user.seed.ts rename to apps/api/src/modules/user/seed/user.seed.ts index 5b46afa..e609900 100644 --- a/apps/server/src/modules/user/seed/user.seed.ts +++ b/apps/api/src/modules/user/seed/user.seed.ts @@ -1,5 +1,5 @@ -import { CredentialsService, UserService } from "@cv/auth"; -import { PrismaService } from "@cv/system"; +import { CredentialsService, UserService } from "@cv/core"; +import { PrismaService } from "@cv/core"; import { Injectable, Logger } from "@nestjs/common"; import { Seeder } from "@/modules/database/seed/seed.service"; import { Seeder as SeederDecorator } from "@/modules/database/seed/seeder.decorator"; diff --git a/apps/server/src/modules/user/user.type.ts b/apps/api/src/modules/user/user.type.ts similarity index 96% rename from apps/server/src/modules/user/user.type.ts rename to apps/api/src/modules/user/user.type.ts index 4595e93..da6a416 100644 --- a/apps/server/src/modules/user/user.type.ts +++ b/apps/api/src/modules/user/user.type.ts @@ -1,5 +1,5 @@ -import type { User as DomainUser } from "@cv/auth"; -import { UserRole } from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; +import { UserRole } from "@cv/core"; import { Field, ID, ObjectType, registerEnumType } from "@nestjs/graphql"; registerEnumType(UserRole, { name: "UserRole" }); diff --git a/apps/api/src/modules/vacancies/contract-type/contract-type.dataloader.ts b/apps/api/src/modules/vacancies/contract-type/contract-type.dataloader.ts new file mode 100644 index 0000000..77cbf40 --- /dev/null +++ b/apps/api/src/modules/vacancies/contract-type/contract-type.dataloader.ts @@ -0,0 +1,15 @@ +import { BaseDataLoaderService } from "@cv/core"; +import { Injectable, Scope } from "@nestjs/common"; +import { ContractType } from "./contract-type.entity"; +import { ContractTypeService } from "./contract-type.service"; + +@Injectable({ scope: Scope.REQUEST }) +export class ContractTypeDataLoaderService extends BaseDataLoaderService { + constructor(service: ContractTypeService) { + super(async (ids: readonly string[]) => { + const items = await service.findMany({ id: [...ids] }); + const map = new Map(items.map((item) => [item.id, item])); + return ids.map((id) => map.get(id) ?? null); + }); + } +} diff --git a/apps/api/src/modules/vacancies/contract-type/contract-type.entity.ts b/apps/api/src/modules/vacancies/contract-type/contract-type.entity.ts new file mode 100644 index 0000000..eec964c --- /dev/null +++ b/apps/api/src/modules/vacancies/contract-type/contract-type.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "@cv/core"; + +export class ContractType extends NamedEntity {} diff --git a/apps/api/src/modules/vacancies/contract-type/contract-type.mapper.ts b/apps/api/src/modules/vacancies/contract-type/contract-type.mapper.ts new file mode 100644 index 0000000..74c1a23 --- /dev/null +++ b/apps/api/src/modules/vacancies/contract-type/contract-type.mapper.ts @@ -0,0 +1,20 @@ +import { createNamedEntityMapper } from "@cv/core"; +import { Injectable } from "@nestjs/common"; +import type { ContractType as PrismaContractType } from "@prisma/client"; +import { ContractType } from "./contract-type.entity"; + +@Injectable() +export class ContractTypeMapper { + private mapper = createNamedEntityMapper(ContractType); + + toDomain(prisma: null): null; + toDomain(prisma: PrismaContractType): ContractType; + toDomain(prisma: PrismaContractType | null): ContractType | null; + toDomain(prisma: PrismaContractType | null): ContractType | null { + return this.mapper.toDomain(prisma); + } + + mapToDomain(prisma: PrismaContractType[]): ContractType[] { + return this.mapper.mapToDomain(prisma); + } +} diff --git a/apps/api/src/modules/vacancies/contract-type/contract-type.module.ts b/apps/api/src/modules/vacancies/contract-type/contract-type.module.ts new file mode 100644 index 0000000..70c411d --- /dev/null +++ b/apps/api/src/modules/vacancies/contract-type/contract-type.module.ts @@ -0,0 +1,12 @@ +import { DatabaseModule } from "@cv/core"; +import { Module } from "@nestjs/common"; +import { ContractTypeDataLoaderService } from "./contract-type.dataloader"; +import { ContractTypeMapper } from "./contract-type.mapper"; +import { ContractTypeService } from "./contract-type.service"; + +@Module({ + imports: [DatabaseModule], + providers: [ContractTypeService, ContractTypeMapper, ContractTypeDataLoaderService], + exports: [ContractTypeService, ContractTypeDataLoaderService], +}) +export class ContractTypeModule {} diff --git a/apps/api/src/modules/vacancies/contract-type/contract-type.service.ts b/apps/api/src/modules/vacancies/contract-type/contract-type.service.ts new file mode 100644 index 0000000..c6ae232 --- /dev/null +++ b/apps/api/src/modules/vacancies/contract-type/contract-type.service.ts @@ -0,0 +1,11 @@ +import { NamedEntityService, PrismaService } from "@cv/core"; +import { Injectable } from "@nestjs/common"; +import { ContractType } from "./contract-type.entity"; +import { ContractTypeMapper } from "./contract-type.mapper"; + +@Injectable() +export class ContractTypeService extends NamedEntityService { + constructor(prisma: PrismaService, mapper: ContractTypeMapper) { + super(prisma, mapper, prisma.contractType, "ContractType"); + } +} diff --git a/apps/api/src/modules/vacancies/contract-type/graphql/contract-type.type.ts b/apps/api/src/modules/vacancies/contract-type/graphql/contract-type.type.ts new file mode 100644 index 0000000..2f10eff --- /dev/null +++ b/apps/api/src/modules/vacancies/contract-type/graphql/contract-type.type.ts @@ -0,0 +1,13 @@ +import { createNamedGraphQLType } from "@/modules/base/named-graphql-type.factory"; +import { ContractType as ContractTypeDomain } from "../contract-type.entity"; + +const { Type, Connection, Edge } = createNamedGraphQLType("ContractType", ContractTypeDomain); + +export const ContractType = Type; +export type ContractType = InstanceType; + +export const ContractTypeConnection = Connection; +export type ContractTypeConnection = InstanceType; + +export const ContractTypeEdge = Edge; +export type ContractTypeEdge = InstanceType; diff --git a/apps/server/src/modules/vacancies/graphql/user-field.resolver.ts b/apps/api/src/modules/vacancies/graphql/user-field.resolver.ts similarity index 95% rename from apps/server/src/modules/vacancies/graphql/user-field.resolver.ts rename to apps/api/src/modules/vacancies/graphql/user-field.resolver.ts index a7a8625..07e5aa9 100644 --- a/apps/server/src/modules/vacancies/graphql/user-field.resolver.ts +++ b/apps/api/src/modules/vacancies/graphql/user-field.resolver.ts @@ -1,5 +1,5 @@ import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; -import { PaginationArgs, PaginationService } from "@cv/system"; +import { PaginationArgs, PaginationService } from "@cv/core"; import { UseGuards } from "@nestjs/common"; import { Args, Parent, ResolveField, Resolver } from "@nestjs/graphql"; import { User } from "@/modules/user/user.type"; diff --git a/apps/server/src/modules/vacancies/graphql/vacancy-filter.input.ts b/apps/api/src/modules/vacancies/graphql/vacancy-filter.input.ts similarity index 100% rename from apps/server/src/modules/vacancies/graphql/vacancy-filter.input.ts rename to apps/api/src/modules/vacancies/graphql/vacancy-filter.input.ts diff --git a/apps/server/src/modules/vacancies/graphql/vacancy.resolver.ts b/apps/api/src/modules/vacancies/graphql/vacancy.resolver.ts similarity index 68% rename from apps/server/src/modules/vacancies/graphql/vacancy.resolver.ts rename to apps/api/src/modules/vacancies/graphql/vacancy.resolver.ts index 03b0621..34da282 100644 --- a/apps/server/src/modules/vacancies/graphql/vacancy.resolver.ts +++ b/apps/api/src/modules/vacancies/graphql/vacancy.resolver.ts @@ -1,9 +1,6 @@ -import type { User as DomainUser } from "@cv/auth"; -import { - AuthorizationService, - JwtAuthGuard, - VerifiedScopeGuard, -} from "@cv/auth"; +import type { User as DomainUser } from "@cv/core"; +import { AuthorizationService } from "@cv/core"; +import { JwtAuthGuard, VerifiedScopeGuard } from "@cv/auth"; import { UseGuards } from "@nestjs/common"; import { Args, @@ -12,6 +9,7 @@ import { ResolveField, Resolver, } from "@nestjs/graphql"; +import { compact } from "@cv/utils"; import { CurrentUser } from "@/modules/current-user/current-user.decorator"; import { CompanyDataLoaderService } from "@/modules/job-experience/company/company.dataloader"; import { Company } from "@/modules/job-experience/company/graphql/company.type"; @@ -21,6 +19,12 @@ import { Role } from "@/modules/job-experience/role/graphql/role.type"; import { RoleDataLoaderService } from "@/modules/job-experience/role/role.dataloader"; import { Skill } from "@/modules/job-experience/skill/graphql/skill.type"; import { SkillService } from "@/modules/job-experience/skill/skill.service"; +import { ContractTypeDataLoaderService } from "@/modules/vacancies/contract-type/contract-type.dataloader"; +import { ContractType } from "@/modules/vacancies/contract-type/graphql/contract-type.type"; +import { LocationDataLoaderService } from "@/modules/vacancies/location/location.dataloader"; +import { Location } from "@/modules/vacancies/location/graphql/location.type"; +import { RateTypeDataLoaderService } from "@/modules/vacancies/rate-type/rate-type.dataloader"; +import { RateType } from "@/modules/vacancies/rate-type/graphql/rate-type.type"; import { Vacancy as VacancyEntity } from "@/modules/vacancies/vacancy.entity"; import { VacancyFactory } from "@/modules/vacancies/vacancy.factory"; import { VacancyService } from "@/modules/vacancies/vacancy.service"; @@ -37,6 +41,9 @@ export class VacancyResolver { private readonly levelDataLoader: LevelDataLoaderService, private readonly skillService: SkillService, private readonly authorizationService: AuthorizationService, + private readonly rateTypeDataLoader: RateTypeDataLoaderService, + private readonly contractTypeDataLoader: ContractTypeDataLoaderService, + private readonly locationDataLoader: LocationDataLoaderService, ) {} @Mutation(() => Vacancy) @@ -49,7 +56,7 @@ export class VacancyResolver { @Args("jobTypeId", { nullable: true }) jobTypeId?: string, @Args("description", { nullable: true }) description?: string, @Args("requirements", { nullable: true }) requirements?: string, - @Args("location", { nullable: true }) location?: string, + @Args("locationId", { nullable: true }) locationId?: string, @Args("minSalary", { nullable: true }) minSalary?: number, @Args("maxSalary", { nullable: true }) maxSalary?: number, @Args("applicationUrl", { nullable: true }) applicationUrl?: string, @@ -79,16 +86,18 @@ export class VacancyResolver { company, role, level, - ...(jobTypeId !== undefined && { jobTypeId }), - ...(description !== undefined && { description }), - ...(requirements !== undefined && { requirements }), - ...(location !== undefined && { location }), - ...(minSalary !== undefined && { minSalary }), - ...(maxSalary !== undefined && { maxSalary }), - ...(applicationUrl !== undefined && { applicationUrl }), - ...(deadline !== undefined && { deadline }), - ...(isActive !== undefined && { isActive }), - ...(isPublic !== undefined && { isPublic }), + ...compact({ + jobTypeId, + description, + requirements, + locationId, + minSalary, + maxSalary, + applicationUrl, + deadline, + isActive, + isPublic, + }), }; const entity = this.vacancyFactory.create({ @@ -142,4 +151,25 @@ export class VacancyResolver { const skills = await this.skillService.findMany({ vacancyId: id }); return skills.map((skill) => Skill.fromDomain(skill)); } + + @ResolveField(() => RateType, { nullable: true }) + async rateType(@Parent() vacancy: Vacancy): Promise { + if (!vacancy.rateTypeId) return null; + const rateType = await this.rateTypeDataLoader.load(vacancy.rateTypeId); + return rateType ? RateType.fromDomain(rateType) : null; + } + + @ResolveField(() => ContractType, { nullable: true }) + async contractType(@Parent() vacancy: Vacancy): Promise { + if (!vacancy.contractTypeId) return null; + const contractType = await this.contractTypeDataLoader.load(vacancy.contractTypeId); + return contractType ? ContractType.fromDomain(contractType) : null; + } + + @ResolveField(() => Location, { nullable: true }) + async location(@Parent() vacancy: Vacancy): Promise { + if (!vacancy.locationId) return null; + const location = await this.locationDataLoader.load(vacancy.locationId); + return location ? Location.fromDomain(location) : null; + } } diff --git a/apps/api/src/modules/vacancies/graphql/vacancy.type.ts b/apps/api/src/modules/vacancies/graphql/vacancy.type.ts new file mode 100644 index 0000000..e2a883c --- /dev/null +++ b/apps/api/src/modules/vacancies/graphql/vacancy.type.ts @@ -0,0 +1,80 @@ +import { Field, ObjectType } from "@nestjs/graphql"; +import { BaseGraphQLType } from "@/modules/base/base-graphql-type"; +import { createConnection } from "@/modules/base/connection.factory"; +import type { Vacancy as DomainVacancy } from "@/modules/vacancies/vacancy.entity"; +import { Vacancy as VacancyEntity } from "@/modules/vacancies/vacancy.entity"; + +@ObjectType() +export class Vacancy extends BaseGraphQLType { + @Field() + title: string; + + @Field() + ownerId: string; + + @Field(() => String, { nullable: true }) + description: string | null; + + @Field(() => String, { nullable: true }) + requirements: string | null; + + @Field(() => Number, { nullable: true }) + minSalary: number | null; + + @Field(() => Number, { nullable: true }) + maxSalary: number | null; + + @Field(() => String, { nullable: true }) + applicationUrl: string | null; + + @Field(() => Date, { nullable: true }) + deadline: Date | null; + + @Field() + isActive: boolean; + + @Field() + isPublic: boolean; + + // Internal FK IDs — not exposed in GraphQL, used by field resolvers + companyId: string; + roleId: string; + levelId: string | null; + jobTypeId: string | null; + rateTypeId: string | null; + contractTypeId: string | null; + locationId: string | null; + + constructor(domain: DomainVacancy) { + super(domain.id, domain.createdAt, domain.updatedAt); + this.title = domain.title; + this.ownerId = domain.ownerId; + this.companyId = domain.company.id; + this.roleId = domain.role.id; + this.levelId = domain.level?.id ?? null; + this.jobTypeId = domain.jobTypeId ?? null; + this.rateTypeId = domain.rateTypeId ?? null; + this.contractTypeId = domain.contractTypeId ?? null; + this.locationId = domain.locationId ?? null; + this.description = domain.description ?? null; + this.requirements = domain.requirements ?? null; + this.minSalary = domain.minSalary ?? null; + this.maxSalary = domain.maxSalary ?? null; + this.applicationUrl = domain.applicationUrl ?? null; + this.deadline = domain.deadline ?? null; + this.isActive = domain.isActive; + this.isPublic = domain.isPublic; + } + + static fromDomain(domain: DomainVacancy): Vacancy { + return new Vacancy(domain); + } +} + +export const { Connection: VacancyConnection, Edge: VacancyEdge } = + createConnection(Vacancy, (domain) => + Vacancy.fromDomain(domain), + ); + +export type VacancyConnection = InstanceType; +export type VacancyEdge = InstanceType; diff --git a/apps/server/src/modules/vacancies/job-type/job-type.dto.ts b/apps/api/src/modules/vacancies/job-type/job-type.dto.ts similarity index 100% rename from apps/server/src/modules/vacancies/job-type/job-type.dto.ts rename to apps/api/src/modules/vacancies/job-type/job-type.dto.ts diff --git a/apps/server/src/modules/vacancies/job-type/job-type.entity.ts b/apps/api/src/modules/vacancies/job-type/job-type.entity.ts similarity index 51% rename from apps/server/src/modules/vacancies/job-type/job-type.entity.ts rename to apps/api/src/modules/vacancies/job-type/job-type.entity.ts index c7e535e..90d5d9a 100644 --- a/apps/server/src/modules/vacancies/job-type/job-type.entity.ts +++ b/apps/api/src/modules/vacancies/job-type/job-type.entity.ts @@ -1,3 +1,3 @@ -import { NamedEntity } from "@cv/system"; +import { NamedEntity } from "@cv/core"; export class JobType extends NamedEntity {} diff --git a/apps/server/src/modules/vacancies/job-type/job-type.factory.ts b/apps/api/src/modules/vacancies/job-type/job-type.factory.ts similarity index 88% rename from apps/server/src/modules/vacancies/job-type/job-type.factory.ts rename to apps/api/src/modules/vacancies/job-type/job-type.factory.ts index 1e826da..6fdf187 100644 --- a/apps/server/src/modules/vacancies/job-type/job-type.factory.ts +++ b/apps/api/src/modules/vacancies/job-type/job-type.factory.ts @@ -1,4 +1,4 @@ -import { ClockService, Factory, UuidFactoryService } from "@cv/system"; +import { ClockService, Factory, UuidFactoryService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { CreateJobTypeDto } from "./job-type.dto"; import { JobType } from "./job-type.entity"; diff --git a/apps/server/src/modules/vacancies/job-type/job-type.mapper.ts b/apps/api/src/modules/vacancies/job-type/job-type.mapper.ts similarity index 92% rename from apps/server/src/modules/vacancies/job-type/job-type.mapper.ts rename to apps/api/src/modules/vacancies/job-type/job-type.mapper.ts index 1ca61c0..e0316f4 100644 --- a/apps/server/src/modules/vacancies/job-type/job-type.mapper.ts +++ b/apps/api/src/modules/vacancies/job-type/job-type.mapper.ts @@ -1,4 +1,4 @@ -import { createNamedEntityMapper } from "@cv/system"; +import { createNamedEntityMapper } from "@cv/core"; import { Injectable } from "@nestjs/common"; import type { JobType as PrismaJobType } from "@prisma/client"; import { JobType } from "./job-type.entity"; diff --git a/apps/server/src/modules/vacancies/job-type/job-type.module.ts b/apps/api/src/modules/vacancies/job-type/job-type.module.ts similarity index 87% rename from apps/server/src/modules/vacancies/job-type/job-type.module.ts rename to apps/api/src/modules/vacancies/job-type/job-type.module.ts index acee13a..9f5ca4d 100644 --- a/apps/server/src/modules/vacancies/job-type/job-type.module.ts +++ b/apps/api/src/modules/vacancies/job-type/job-type.module.ts @@ -1,4 +1,4 @@ -import { BaseModule, DatabaseModule } from "@cv/system"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { JobTypeFactory } from "./job-type.factory"; import { JobTypeMapper } from "./job-type.mapper"; diff --git a/apps/server/src/modules/vacancies/job-type/job-type.service.ts b/apps/api/src/modules/vacancies/job-type/job-type.service.ts similarity index 84% rename from apps/server/src/modules/vacancies/job-type/job-type.service.ts rename to apps/api/src/modules/vacancies/job-type/job-type.service.ts index 6aeed1a..316f60c 100644 --- a/apps/server/src/modules/vacancies/job-type/job-type.service.ts +++ b/apps/api/src/modules/vacancies/job-type/job-type.service.ts @@ -1,4 +1,4 @@ -import { NamedEntityService, PrismaService } from "@cv/system"; +import { NamedEntityService, PrismaService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { JobType } from "./job-type.entity"; import { JobTypeMapper } from "./job-type.mapper"; diff --git a/apps/api/src/modules/vacancies/location/graphql/location.type.ts b/apps/api/src/modules/vacancies/location/graphql/location.type.ts new file mode 100644 index 0000000..25d14a9 --- /dev/null +++ b/apps/api/src/modules/vacancies/location/graphql/location.type.ts @@ -0,0 +1,35 @@ +import { Field, ID, ObjectType, registerEnumType } from "@nestjs/graphql"; +import { BaseGraphQLType } from "@/modules/base/base-graphql-type"; +import { + LocationType, + type Location as LocationDomain, +} from "../location.entity"; + +registerEnumType(LocationType, { name: "LocationType" }); + +@ObjectType() +export class Location extends BaseGraphQLType { + @Field() + name: string; + + @Field(() => LocationType) + type: LocationType; + + @Field(() => String, { nullable: true }) + code?: string | undefined; + + @Field(() => ID, { nullable: true }) + parentId?: string | undefined; + + constructor(domain: LocationDomain) { + super(domain.id, domain.createdAt, domain.updatedAt); + this.name = domain.name; + this.type = domain.type; + this.code = domain.code; + this.parentId = domain.parentId; + } + + static fromDomain(domain: LocationDomain): Location { + return new Location(domain); + } +} diff --git a/apps/api/src/modules/vacancies/location/location.dataloader.ts b/apps/api/src/modules/vacancies/location/location.dataloader.ts new file mode 100644 index 0000000..6a4bd60 --- /dev/null +++ b/apps/api/src/modules/vacancies/location/location.dataloader.ts @@ -0,0 +1,15 @@ +import { BaseDataLoaderService } from "@cv/core"; +import { Injectable, Scope } from "@nestjs/common"; +import { Location } from "./location.entity"; +import { LocationService } from "./location.service"; + +@Injectable({ scope: Scope.REQUEST }) +export class LocationDataLoaderService extends BaseDataLoaderService { + constructor(service: LocationService) { + super(async (ids: readonly string[]) => { + const items = await service.findMany({ id: [...ids] }); + const map = new Map(items.map((item) => [item.id, item])); + return ids.map((id) => map.get(id) ?? null); + }); + } +} diff --git a/apps/api/src/modules/vacancies/location/location.entity.ts b/apps/api/src/modules/vacancies/location/location.entity.ts new file mode 100644 index 0000000..8dc395d --- /dev/null +++ b/apps/api/src/modules/vacancies/location/location.entity.ts @@ -0,0 +1,33 @@ +import { BaseEntity } from "@cv/core"; + +export const LocationType = { + COUNTRY: "COUNTRY", + PROVINCE: "PROVINCE", + CITY: "CITY", + DISTRICT: "DISTRICT", +} as const; + +export type LocationType = (typeof LocationType)[keyof typeof LocationType]; + +export class Location extends BaseEntity { + name: string; + type: LocationType; + code?: string | undefined; + parentId?: string | undefined; + + constructor( + id: string, + name: string, + type: LocationType, + createdAt: Date, + updatedAt: Date, + code?: string, + parentId?: string, + ) { + super(id, createdAt, updatedAt); + this.name = name; + this.type = type; + this.code = code; + this.parentId = parentId; + } +} diff --git a/apps/api/src/modules/vacancies/location/location.mapper.ts b/apps/api/src/modules/vacancies/location/location.mapper.ts new file mode 100644 index 0000000..2c93931 --- /dev/null +++ b/apps/api/src/modules/vacancies/location/location.mapper.ts @@ -0,0 +1,26 @@ +import { Injectable } from "@nestjs/common"; +import type { Location as PrismaLocation } from "@prisma/client"; +import { Location } from "./location.entity"; + +@Injectable() +export class LocationMapper { + toDomain(prisma: null): null; + toDomain(prisma: PrismaLocation): Location; + toDomain(prisma: PrismaLocation | null): Location | null; + toDomain(prisma: PrismaLocation | null): Location | null { + if (!prisma) return null; + return new Location( + prisma.id, + prisma.name, + prisma.type, + prisma.createdAt, + prisma.updatedAt, + prisma.code ?? undefined, + prisma.parentId ?? undefined, + ); + } + + mapToDomain(items: PrismaLocation[]): Location[] { + return items.map((item) => this.toDomain(item)).filter((item): item is Location => item !== null); + } +} diff --git a/apps/api/src/modules/vacancies/location/location.module.ts b/apps/api/src/modules/vacancies/location/location.module.ts new file mode 100644 index 0000000..498e21d --- /dev/null +++ b/apps/api/src/modules/vacancies/location/location.module.ts @@ -0,0 +1,12 @@ +import { DatabaseModule } from "@cv/core"; +import { Module } from "@nestjs/common"; +import { LocationDataLoaderService } from "./location.dataloader"; +import { LocationMapper } from "./location.mapper"; +import { LocationService } from "./location.service"; + +@Module({ + imports: [DatabaseModule], + providers: [LocationService, LocationMapper, LocationDataLoaderService], + exports: [LocationService, LocationDataLoaderService], +}) +export class LocationModule {} diff --git a/apps/api/src/modules/vacancies/location/location.service.ts b/apps/api/src/modules/vacancies/location/location.service.ts new file mode 100644 index 0000000..6179308 --- /dev/null +++ b/apps/api/src/modules/vacancies/location/location.service.ts @@ -0,0 +1,23 @@ +import { PrismaService } from "@cv/core"; +import { Injectable } from "@nestjs/common"; +import { Location } from "./location.entity"; +import { LocationMapper } from "./location.mapper"; + +@Injectable() +export class LocationService { + constructor( + private readonly prisma: PrismaService, + private readonly mapper: LocationMapper, + ) {} + + async findById(id: string): Promise { + const location = await this.prisma.location.findUnique({ where: { id } }); + return this.mapper.toDomain(location); + } + + async findMany(filter?: { id?: string[] }): Promise { + const where = filter?.id ? { id: { in: filter.id } } : {}; + const items = await this.prisma.location.findMany({ where }); + return this.mapper.mapToDomain(items); + } +} diff --git a/apps/api/src/modules/vacancies/rate-type/graphql/rate-type.type.ts b/apps/api/src/modules/vacancies/rate-type/graphql/rate-type.type.ts new file mode 100644 index 0000000..015cfb5 --- /dev/null +++ b/apps/api/src/modules/vacancies/rate-type/graphql/rate-type.type.ts @@ -0,0 +1,13 @@ +import { createNamedGraphQLType } from "@/modules/base/named-graphql-type.factory"; +import { RateType as RateTypeDomain } from "../rate-type.entity"; + +const { Type, Connection, Edge } = createNamedGraphQLType("RateType", RateTypeDomain); + +export const RateType = Type; +export type RateType = InstanceType; + +export const RateTypeConnection = Connection; +export type RateTypeConnection = InstanceType; + +export const RateTypeEdge = Edge; +export type RateTypeEdge = InstanceType; diff --git a/apps/api/src/modules/vacancies/rate-type/rate-type.dataloader.ts b/apps/api/src/modules/vacancies/rate-type/rate-type.dataloader.ts new file mode 100644 index 0000000..15b7622 --- /dev/null +++ b/apps/api/src/modules/vacancies/rate-type/rate-type.dataloader.ts @@ -0,0 +1,15 @@ +import { BaseDataLoaderService } from "@cv/core"; +import { Injectable, Scope } from "@nestjs/common"; +import { RateType } from "./rate-type.entity"; +import { RateTypeService } from "./rate-type.service"; + +@Injectable({ scope: Scope.REQUEST }) +export class RateTypeDataLoaderService extends BaseDataLoaderService { + constructor(service: RateTypeService) { + super(async (ids: readonly string[]) => { + const items = await service.findMany({ id: [...ids] }); + const map = new Map(items.map((item) => [item.id, item])); + return ids.map((id) => map.get(id) ?? null); + }); + } +} diff --git a/apps/api/src/modules/vacancies/rate-type/rate-type.entity.ts b/apps/api/src/modules/vacancies/rate-type/rate-type.entity.ts new file mode 100644 index 0000000..9b5ae55 --- /dev/null +++ b/apps/api/src/modules/vacancies/rate-type/rate-type.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "@cv/core"; + +export class RateType extends NamedEntity {} diff --git a/apps/api/src/modules/vacancies/rate-type/rate-type.mapper.ts b/apps/api/src/modules/vacancies/rate-type/rate-type.mapper.ts new file mode 100644 index 0000000..3a990a0 --- /dev/null +++ b/apps/api/src/modules/vacancies/rate-type/rate-type.mapper.ts @@ -0,0 +1,20 @@ +import { createNamedEntityMapper } from "@cv/core"; +import { Injectable } from "@nestjs/common"; +import type { RateType as PrismaRateType } from "@prisma/client"; +import { RateType } from "./rate-type.entity"; + +@Injectable() +export class RateTypeMapper { + private mapper = createNamedEntityMapper(RateType); + + toDomain(prisma: null): null; + toDomain(prisma: PrismaRateType): RateType; + toDomain(prisma: PrismaRateType | null): RateType | null; + toDomain(prisma: PrismaRateType | null): RateType | null { + return this.mapper.toDomain(prisma); + } + + mapToDomain(prisma: PrismaRateType[]): RateType[] { + return this.mapper.mapToDomain(prisma); + } +} diff --git a/apps/api/src/modules/vacancies/rate-type/rate-type.module.ts b/apps/api/src/modules/vacancies/rate-type/rate-type.module.ts new file mode 100644 index 0000000..6fd4e07 --- /dev/null +++ b/apps/api/src/modules/vacancies/rate-type/rate-type.module.ts @@ -0,0 +1,12 @@ +import { DatabaseModule } from "@cv/core"; +import { Module } from "@nestjs/common"; +import { RateTypeDataLoaderService } from "./rate-type.dataloader"; +import { RateTypeMapper } from "./rate-type.mapper"; +import { RateTypeService } from "./rate-type.service"; + +@Module({ + imports: [DatabaseModule], + providers: [RateTypeService, RateTypeMapper, RateTypeDataLoaderService], + exports: [RateTypeService, RateTypeDataLoaderService], +}) +export class RateTypeModule {} diff --git a/apps/api/src/modules/vacancies/rate-type/rate-type.service.ts b/apps/api/src/modules/vacancies/rate-type/rate-type.service.ts new file mode 100644 index 0000000..e657c2c --- /dev/null +++ b/apps/api/src/modules/vacancies/rate-type/rate-type.service.ts @@ -0,0 +1,11 @@ +import { NamedEntityService, PrismaService } from "@cv/core"; +import { Injectable } from "@nestjs/common"; +import { RateType } from "./rate-type.entity"; +import { RateTypeMapper } from "./rate-type.mapper"; + +@Injectable() +export class RateTypeService extends NamedEntityService { + constructor(prisma: PrismaService, mapper: RateTypeMapper) { + super(prisma, mapper, prisma.rateType, "RateType"); + } +} diff --git a/apps/server/src/modules/vacancies/seed/vacancy.seed.ts b/apps/api/src/modules/vacancies/seed/vacancy.seed.ts similarity index 97% rename from apps/server/src/modules/vacancies/seed/vacancy.seed.ts rename to apps/api/src/modules/vacancies/seed/vacancy.seed.ts index fca875c..1b476eb 100644 --- a/apps/server/src/modules/vacancies/seed/vacancy.seed.ts +++ b/apps/api/src/modules/vacancies/seed/vacancy.seed.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "@cv/core"; import { faker } from "@faker-js/faker"; import { Injectable, Logger } from "@nestjs/common"; import { Seeder } from "@/modules/database/seed/seed.service"; @@ -66,7 +66,6 @@ export class VacancySeedService implements Seeder { jobTypeId: jobType.id, description: faker.lorem.paragraphs(3), requirements: faker.lorem.paragraphs(2), - location: faker.location.city(), minSalary, maxSalary, applicationUrl: faker.internet.url(), diff --git a/apps/server/src/modules/vacancies/vacancy.dataloader.ts b/apps/api/src/modules/vacancies/vacancy.dataloader.ts similarity index 92% rename from apps/server/src/modules/vacancies/vacancy.dataloader.ts rename to apps/api/src/modules/vacancies/vacancy.dataloader.ts index a92a44c..43d7c00 100644 --- a/apps/server/src/modules/vacancies/vacancy.dataloader.ts +++ b/apps/api/src/modules/vacancies/vacancy.dataloader.ts @@ -1,4 +1,4 @@ -import { BaseDataLoaderService } from "@cv/system"; +import { BaseDataLoaderService } from "@cv/core"; import { Injectable, Scope } from "@nestjs/common"; import { Vacancy } from "./vacancy.entity"; import { VacancyService } from "./vacancy.service"; diff --git a/apps/server/src/modules/vacancies/vacancy.entity.ts b/apps/api/src/modules/vacancies/vacancy.entity.ts similarity index 73% rename from apps/server/src/modules/vacancies/vacancy.entity.ts rename to apps/api/src/modules/vacancies/vacancy.entity.ts index 1f31de7..1bc90b3 100644 --- a/apps/server/src/modules/vacancies/vacancy.entity.ts +++ b/apps/api/src/modules/vacancies/vacancy.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "@cv/core"; import { Company } from "@/modules/job-experience/company/company.entity"; import { Level } from "@/modules/job-experience/level/level.entity"; import { Role } from "@/modules/job-experience/role/role.entity"; @@ -11,15 +11,27 @@ export class Vacancy extends BaseEntity { level: Level | null; jobTypeId?: string; description?: string; - requirements?: string; - location?: string; - minSalary?: number; - maxSalary?: number; - applicationUrl?: string; - deadline?: Date; + requirements?: string | undefined; + minSalary?: number | undefined; + maxSalary?: number | undefined; + applicationUrl?: string | undefined; + deadline?: Date | undefined; + hoursFrom?: number | undefined; + hoursTo?: number | undefined; isActive: boolean; isPublic: boolean; + // Entity FKs + rateTypeId?: string | undefined; + contractTypeId?: string | undefined; + locationId?: string | undefined; + + // External import fields + externalId?: string | undefined; + source?: string | undefined; + externalApplyUrl?: string | undefined; + publishedAt?: Date | undefined; + constructor( id: string, title: string, @@ -32,7 +44,7 @@ export class Vacancy extends BaseEntity { jobTypeId?: string, description?: string, requirements?: string, - location?: string, + _location?: string, minSalary?: number, maxSalary?: number, applicationUrl?: string, @@ -58,9 +70,6 @@ export class Vacancy extends BaseEntity { if (requirements !== undefined) { this.requirements = requirements; } - if (location !== undefined) { - this.location = location; - } if (minSalary !== undefined) { this.minSalary = minSalary; } diff --git a/apps/server/src/modules/vacancies/vacancy.factory.ts b/apps/api/src/modules/vacancies/vacancy.factory.ts similarity index 92% rename from apps/server/src/modules/vacancies/vacancy.factory.ts rename to apps/api/src/modules/vacancies/vacancy.factory.ts index 8c32002..8e876d7 100644 --- a/apps/server/src/modules/vacancies/vacancy.factory.ts +++ b/apps/api/src/modules/vacancies/vacancy.factory.ts @@ -1,4 +1,4 @@ -import { ClockService, Factory, UuidFactoryService } from "@cv/system"; +import { ClockService, Factory, UuidFactoryService } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Company } from "@/modules/job-experience/company/company.entity"; import { Level } from "@/modules/job-experience/level/level.entity"; @@ -14,7 +14,7 @@ export interface CreateVacancyDto { jobTypeId?: string; description?: string; requirements?: string; - location?: string; + locationId?: string; minSalary?: number; maxSalary?: number; applicationUrl?: string; @@ -44,7 +44,7 @@ export class VacancyFactory implements Factory { createVacancyDto.jobTypeId, createVacancyDto.description, createVacancyDto.requirements, - createVacancyDto.location, + undefined, createVacancyDto.minSalary, createVacancyDto.maxSalary, createVacancyDto.applicationUrl, diff --git a/apps/server/src/modules/vacancies/vacancy.mapper.ts b/apps/api/src/modules/vacancies/vacancy.mapper.ts similarity index 75% rename from apps/server/src/modules/vacancies/vacancy.mapper.ts rename to apps/api/src/modules/vacancies/vacancy.mapper.ts index 1a6a17d..f1841b0 100644 --- a/apps/server/src/modules/vacancies/vacancy.mapper.ts +++ b/apps/api/src/modules/vacancies/vacancy.mapper.ts @@ -42,7 +42,7 @@ export class VacancyMapper { ? this.levelMapper.toDomain(prismaVacancy.level) : null; - return new Vacancy( + const vacancy = new Vacancy( prismaVacancy.id, prismaVacancy.title, prismaVacancy.ownerId, @@ -54,7 +54,7 @@ export class VacancyMapper { prismaVacancy.jobTypeId ?? undefined, prismaVacancy.description ?? undefined, prismaVacancy.requirements ?? undefined, - prismaVacancy.location ?? undefined, + undefined, prismaVacancy.minSalary ?? undefined, prismaVacancy.maxSalary ?? undefined, prismaVacancy.applicationUrl ?? undefined, @@ -62,6 +62,18 @@ export class VacancyMapper { prismaVacancy.isActive, prismaVacancy.isPublic, ); + + vacancy.hoursFrom = prismaVacancy.hoursFrom ?? undefined; + vacancy.hoursTo = prismaVacancy.hoursTo ?? undefined; + vacancy.rateTypeId = prismaVacancy.rateTypeId ?? undefined; + vacancy.contractTypeId = prismaVacancy.contractTypeId ?? undefined; + vacancy.locationId = prismaVacancy.locationId ?? undefined; + vacancy.externalId = prismaVacancy.externalId ?? undefined; + vacancy.source = prismaVacancy.source ?? undefined; + vacancy.externalApplyUrl = prismaVacancy.externalApplyUrl ?? undefined; + vacancy.publishedAt = prismaVacancy.publishedAt ?? undefined; + + return vacancy; } mapToDomain(prismaVacancies: PrismaVacancyWithRelations[]): Vacancy[] { diff --git a/apps/server/src/modules/vacancies/vacancy.module.ts b/apps/api/src/modules/vacancies/vacancy.module.ts similarity index 79% rename from apps/server/src/modules/vacancies/vacancy.module.ts rename to apps/api/src/modules/vacancies/vacancy.module.ts index 5e6f45e..e8620ca 100644 --- a/apps/server/src/modules/vacancies/vacancy.module.ts +++ b/apps/api/src/modules/vacancies/vacancy.module.ts @@ -1,13 +1,16 @@ -import { AuthorizationModule } from "@cv/auth"; -import { BaseModule, DatabaseModule } from "@cv/system"; +import { AuthorizationModule } from "@cv/core"; +import { BaseModule, DatabaseModule } from "@cv/core"; import { Module } from "@nestjs/common"; import { AuthenticationModule } from "@/modules/authentication/authentication.module"; import { CompanyModule } from "@/modules/job-experience/company/company.module"; import { LevelModule } from "@/modules/job-experience/level/level.module"; import { RoleModule } from "@/modules/job-experience/role/role.module"; import { SkillModule } from "@/modules/job-experience/skill/skill.module"; +import { ContractTypeModule } from "./contract-type/contract-type.module"; import { VacancyUserFieldResolver } from "./graphql/user-field.resolver"; import { VacancyResolver } from "./graphql/vacancy.resolver"; +import { LocationModule } from "./location/location.module"; +import { RateTypeModule } from "./rate-type/rate-type.module"; import { VacancySeedService } from "./seed/vacancy.seed"; import { VacancyDataLoaderService } from "./vacancy.dataloader"; import { VacancyFactory } from "./vacancy.factory"; @@ -23,6 +26,9 @@ import { VacancyService } from "./vacancy.service"; RoleModule, LevelModule, SkillModule, + RateTypeModule, + ContractTypeModule, + LocationModule, AuthenticationModule, AuthorizationModule, ], diff --git a/apps/server/src/modules/vacancies/vacancy.policy.ts b/apps/api/src/modules/vacancies/vacancy.policy.ts similarity index 77% rename from apps/server/src/modules/vacancies/vacancy.policy.ts rename to apps/api/src/modules/vacancies/vacancy.policy.ts index aee1cf1..39e240c 100644 --- a/apps/server/src/modules/vacancies/vacancy.policy.ts +++ b/apps/api/src/modules/vacancies/vacancy.policy.ts @@ -1,5 +1,5 @@ -import type { User } from "@cv/auth"; -import { OwnerOwnedResourcePolicy, Policy } from "@cv/auth"; +import type { User } from "@cv/core"; +import { OwnerOwnedResourcePolicy, Policy } from "@cv/core"; import { Injectable } from "@nestjs/common"; import { Vacancy } from "./vacancy.entity"; diff --git a/apps/server/src/modules/vacancies/vacancy.service.ts b/apps/api/src/modules/vacancies/vacancy.service.ts similarity index 88% rename from apps/server/src/modules/vacancies/vacancy.service.ts rename to apps/api/src/modules/vacancies/vacancy.service.ts index 73e4739..913a225 100644 --- a/apps/server/src/modules/vacancies/vacancy.service.ts +++ b/apps/api/src/modules/vacancies/vacancy.service.ts @@ -1,5 +1,5 @@ -import { notFound } from "@cv/auth"; -import { type EntityService, PrismaService, raise } from "@cv/system"; +import { notFound } from "@cv/core"; +import { type EntityService, PrismaService, raise } from "@cv/core"; import { ensureArray } from "@cv/utils"; import { Injectable } from "@nestjs/common"; import type { Prisma } from "@prisma/client"; @@ -67,7 +67,7 @@ export class VacancyService implements EntityService { { requirements: searchFilter, }, - { location: searchFilter }, + { location: { name: searchFilter } }, ]; if (where.OR) { @@ -85,7 +85,7 @@ export class VacancyService implements EntityService { // Location filter if (filter.locations && filter.locations.length > 0) { - where.location = { in: filter.locations }; + where.locationId = { in: filter.locations }; } // Level filter @@ -148,22 +148,7 @@ export class VacancyService implements EntityService { return vacancy ?? notFound("Vacancy", "id", id); } - private buildPrismaData(entity: Vacancy): { - title: string; - companyId: string; - roleId: string; - levelId: string | null; - jobTypeId: string | null; - description: string | null; - requirements: string | null; - location: string | null; - minSalary: number | null; - maxSalary: number | null; - applicationUrl: string | null; - deadline: Date | null; - isActive: boolean; - isPublic: boolean; - } { + private buildPrismaData(entity: Vacancy) { return { title: entity.title, companyId: entity.company.id, @@ -172,7 +157,7 @@ export class VacancyService implements EntityService { jobTypeId: entity.jobTypeId ?? null, description: entity.description ?? null, requirements: entity.requirements ?? null, - location: entity.location ?? null, + locationId: entity.locationId ?? null, minSalary: entity.minSalary ?? null, maxSalary: entity.maxSalary ?? null, applicationUrl: entity.applicationUrl ?? null, diff --git a/apps/server/src/scripts/e2e-coverage-all.cjs b/apps/api/src/scripts/e2e-coverage-all.cjs similarity index 100% rename from apps/server/src/scripts/e2e-coverage-all.cjs rename to apps/api/src/scripts/e2e-coverage-all.cjs diff --git a/apps/server/src/scripts/seed-test.ts b/apps/api/src/scripts/seed-test.ts similarity index 100% rename from apps/server/src/scripts/seed-test.ts rename to apps/api/src/scripts/seed-test.ts diff --git a/apps/server/src/scripts/seed.ts b/apps/api/src/scripts/seed.ts similarity index 100% rename from apps/server/src/scripts/seed.ts rename to apps/api/src/scripts/seed.ts diff --git a/apps/server/test/app-integration.e2e-spec.ts b/apps/api/test/app-integration.e2e-spec.ts similarity index 100% rename from apps/server/test/app-integration.e2e-spec.ts rename to apps/api/test/app-integration.e2e-spec.ts diff --git a/apps/server/test/assets/.gitkeep b/apps/api/test/assets/.gitkeep similarity index 100% rename from apps/server/test/assets/.gitkeep rename to apps/api/test/assets/.gitkeep diff --git a/apps/server/test/auth-integration.e2e-spec.ts b/apps/api/test/auth-integration.e2e-spec.ts similarity index 100% rename from apps/server/test/auth-integration.e2e-spec.ts rename to apps/api/test/auth-integration.e2e-spec.ts diff --git a/apps/server/test/auth-refresh-token.e2e-spec.ts b/apps/api/test/auth-refresh-token.e2e-spec.ts similarity index 100% rename from apps/server/test/auth-refresh-token.e2e-spec.ts rename to apps/api/test/auth-refresh-token.e2e-spec.ts diff --git a/apps/server/test/auth.e2e-spec.ts b/apps/api/test/auth.e2e-spec.ts similarity index 100% rename from apps/server/test/auth.e2e-spec.ts rename to apps/api/test/auth.e2e-spec.ts diff --git a/apps/server/test/cv-upload-integration.e2e-spec.ts b/apps/api/test/cv-upload-integration.e2e-spec.ts similarity index 100% rename from apps/server/test/cv-upload-integration.e2e-spec.ts rename to apps/api/test/cv-upload-integration.e2e-spec.ts diff --git a/apps/server/test/cv-upload.e2e-spec.ts b/apps/api/test/cv-upload.e2e-spec.ts similarity index 100% rename from apps/server/test/cv-upload.e2e-spec.ts rename to apps/api/test/cv-upload.e2e-spec.ts diff --git a/apps/server/test/cv-upload.integration.ts b/apps/api/test/cv-upload.integration.ts similarity index 100% rename from apps/server/test/cv-upload.integration.ts rename to apps/api/test/cv-upload.integration.ts diff --git a/apps/server/test/graphql-optimization.e2e-spec.ts b/apps/api/test/graphql-optimization.e2e-spec.ts similarity index 100% rename from apps/server/test/graphql-optimization.e2e-spec.ts rename to apps/api/test/graphql-optimization.e2e-spec.ts diff --git a/apps/server/test/job-experience-integration.e2e-spec.ts b/apps/api/test/job-experience-integration.e2e-spec.ts similarity index 100% rename from apps/server/test/job-experience-integration.e2e-spec.ts rename to apps/api/test/job-experience-integration.e2e-spec.ts diff --git a/apps/server/test/pagination-integration.e2e-spec.ts b/apps/api/test/pagination-integration.e2e-spec.ts similarity index 100% rename from apps/server/test/pagination-integration.e2e-spec.ts rename to apps/api/test/pagination-integration.e2e-spec.ts diff --git a/apps/server/test/pdf-extraction.integration.ts b/apps/api/test/pdf-extraction.integration.ts similarity index 100% rename from apps/server/test/pdf-extraction.integration.ts rename to apps/api/test/pdf-extraction.integration.ts diff --git a/apps/server/test/queries/all-companies-query.graphql b/apps/api/test/queries/all-companies-query.graphql similarity index 100% rename from apps/server/test/queries/all-companies-query.graphql rename to apps/api/test/queries/all-companies-query.graphql diff --git a/apps/server/test/queries/all-levels-query.graphql b/apps/api/test/queries/all-levels-query.graphql similarity index 100% rename from apps/server/test/queries/all-levels-query.graphql rename to apps/api/test/queries/all-levels-query.graphql diff --git a/apps/server/test/queries/all-roles-query.graphql b/apps/api/test/queries/all-roles-query.graphql similarity index 100% rename from apps/server/test/queries/all-roles-query.graphql rename to apps/api/test/queries/all-roles-query.graphql diff --git a/apps/server/test/queries/all-skills-query.graphql b/apps/api/test/queries/all-skills-query.graphql similarity index 100% rename from apps/server/test/queries/all-skills-query.graphql rename to apps/api/test/queries/all-skills-query.graphql diff --git a/apps/server/test/queries/basic-me-query.graphql b/apps/api/test/queries/basic-me-query.graphql similarity index 100% rename from apps/server/test/queries/basic-me-query.graphql rename to apps/api/test/queries/basic-me-query.graphql diff --git a/apps/server/test/queries/companies-pagination-query.graphql b/apps/api/test/queries/companies-pagination-query.graphql similarity index 100% rename from apps/server/test/queries/companies-pagination-query.graphql rename to apps/api/test/queries/companies-pagination-query.graphql diff --git a/apps/server/test/queries/complex-me-query.graphql b/apps/api/test/queries/complex-me-query.graphql similarity index 100% rename from apps/server/test/queries/complex-me-query.graphql rename to apps/api/test/queries/complex-me-query.graphql diff --git a/apps/server/test/queries/create-company-mutation.graphql b/apps/api/test/queries/create-company-mutation.graphql similarity index 100% rename from apps/server/test/queries/create-company-mutation.graphql rename to apps/api/test/queries/create-company-mutation.graphql diff --git a/apps/server/test/queries/create-job-experience-mutation.graphql b/apps/api/test/queries/create-job-experience-mutation.graphql similarity index 100% rename from apps/server/test/queries/create-job-experience-mutation.graphql rename to apps/api/test/queries/create-job-experience-mutation.graphql diff --git a/apps/server/test/queries/delete-job-experience-mutation.graphql b/apps/api/test/queries/delete-job-experience-mutation.graphql similarity index 100% rename from apps/server/test/queries/delete-job-experience-mutation.graphql rename to apps/api/test/queries/delete-job-experience-mutation.graphql diff --git a/apps/server/test/queries/health-query.graphql b/apps/api/test/queries/health-query.graphql similarity index 100% rename from apps/server/test/queries/health-query.graphql rename to apps/api/test/queries/health-query.graphql diff --git a/apps/server/test/queries/hello-query.graphql b/apps/api/test/queries/hello-query.graphql similarity index 100% rename from apps/server/test/queries/hello-query.graphql rename to apps/api/test/queries/hello-query.graphql diff --git a/apps/server/test/queries/introspection-query.graphql b/apps/api/test/queries/introspection-query.graphql similarity index 100% rename from apps/server/test/queries/introspection-query.graphql rename to apps/api/test/queries/introspection-query.graphql diff --git a/apps/server/test/queries/levels-pagination-query.graphql b/apps/api/test/queries/levels-pagination-query.graphql similarity index 100% rename from apps/server/test/queries/levels-pagination-query.graphql rename to apps/api/test/queries/levels-pagination-query.graphql diff --git a/apps/server/test/queries/load-queries.ts b/apps/api/test/queries/load-queries.ts similarity index 100% rename from apps/server/test/queries/load-queries.ts rename to apps/api/test/queries/load-queries.ts diff --git a/apps/server/test/queries/login-mutation.graphql b/apps/api/test/queries/login-mutation.graphql similarity index 100% rename from apps/server/test/queries/login-mutation.graphql rename to apps/api/test/queries/login-mutation.graphql diff --git a/apps/server/test/queries/me-with-organizations-partial-query.graphql b/apps/api/test/queries/me-with-organizations-partial-query.graphql similarity index 100% rename from apps/server/test/queries/me-with-organizations-partial-query.graphql rename to apps/api/test/queries/me-with-organizations-partial-query.graphql diff --git a/apps/server/test/queries/me-with-organizations-query.graphql b/apps/api/test/queries/me-with-organizations-query.graphql similarity index 100% rename from apps/server/test/queries/me-with-organizations-query.graphql rename to apps/api/test/queries/me-with-organizations-query.graphql diff --git a/apps/server/test/queries/my-employment-history-minimal.graphql b/apps/api/test/queries/my-employment-history-minimal.graphql similarity index 100% rename from apps/server/test/queries/my-employment-history-minimal.graphql rename to apps/api/test/queries/my-employment-history-minimal.graphql diff --git a/apps/server/test/queries/my-employment-history-query.graphql b/apps/api/test/queries/my-employment-history-query.graphql similarity index 100% rename from apps/server/test/queries/my-employment-history-query.graphql rename to apps/api/test/queries/my-employment-history-query.graphql diff --git a/apps/server/test/queries/my-employment-history-simple-query.graphql b/apps/api/test/queries/my-employment-history-simple-query.graphql similarity index 100% rename from apps/server/test/queries/my-employment-history-simple-query.graphql rename to apps/api/test/queries/my-employment-history-simple-query.graphql diff --git a/apps/server/test/queries/register-mutation.graphql b/apps/api/test/queries/register-mutation.graphql similarity index 100% rename from apps/server/test/queries/register-mutation.graphql rename to apps/api/test/queries/register-mutation.graphql diff --git a/apps/server/test/queries/roles-pagination-query.graphql b/apps/api/test/queries/roles-pagination-query.graphql similarity index 100% rename from apps/server/test/queries/roles-pagination-query.graphql rename to apps/api/test/queries/roles-pagination-query.graphql diff --git a/apps/server/test/queries/skills-pagination-query.graphql b/apps/api/test/queries/skills-pagination-query.graphql similarity index 100% rename from apps/server/test/queries/skills-pagination-query.graphql rename to apps/api/test/queries/skills-pagination-query.graphql diff --git a/apps/server/test/queries/update-job-experience-mutation.graphql b/apps/api/test/queries/update-job-experience-mutation.graphql similarity index 100% rename from apps/server/test/queries/update-job-experience-mutation.graphql rename to apps/api/test/queries/update-job-experience-mutation.graphql diff --git a/apps/server/test/test-utils.ts b/apps/api/test/test-utils.ts similarity index 100% rename from apps/server/test/test-utils.ts rename to apps/api/test/test-utils.ts diff --git a/apps/server/tsconfig.build.json b/apps/api/tsconfig.build.json similarity index 100% rename from apps/server/tsconfig.build.json rename to apps/api/tsconfig.build.json diff --git a/apps/server/tsconfig.json b/apps/api/tsconfig.json similarity index 79% rename from apps/server/tsconfig.json rename to apps/api/tsconfig.json index 5756b0e..6cd529d 100644 --- a/apps/server/tsconfig.json +++ b/apps/api/tsconfig.json @@ -6,11 +6,12 @@ "types": ["node"], "experimentalDecorators": true, "emitDecoratorMetadata": true, - "preserveSymlinks": true, + "resolveJsonModule": true, + "preserveSymlinks": false, "paths": { "@/*": ["./src/*"], "@cv/auth": ["../../packages/auth/src"], - "@cv/system": ["../../packages/system/src"], + "@cv/core": ["../../packages/core/src"], "@cv/utils": ["../../packages/utils"] }, "noUncheckedIndexedAccess": true, @@ -24,6 +25,6 @@ "strictPropertyInitialization": true, "noImplicitAny": true }, - "include": ["src/**/*.ts", "src/**/*.d.ts"], + "include": ["src/**/*.ts", "src/**/*.d.ts", "../../packages/tsconfig/graphql.d.ts"], "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] } diff --git a/apps/server/tsup.config.ts b/apps/api/tsup.config.ts similarity index 74% rename from apps/server/tsup.config.ts rename to apps/api/tsup.config.ts index cd92581..079220b 100644 --- a/apps/server/tsup.config.ts +++ b/apps/api/tsup.config.ts @@ -1,7 +1,19 @@ +import { readFileSync } from "node:fs"; import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/main.ts", "src/scripts/seed.ts"], + esbuildPlugins: [ + { + name: "graphql-loader", + setup(build) { + build.onLoad({ filter: /\.graphql$/ }, (args) => ({ + contents: `export default ${JSON.stringify(readFileSync(args.path, "utf8"))}`, + loader: "js", + })); + }, + }, + ], outDir: "dist", format: ["cjs"], target: "node22", @@ -22,7 +34,6 @@ export default defineConfig({ "graphql", /^graphql-/, "@apollo/server", - "apollo-server-express", "express", "cookie-parser", "multer", diff --git a/apps/api/vitest.config.ts b/apps/api/vitest.config.ts new file mode 100644 index 0000000..a4c94c0 --- /dev/null +++ b/apps/api/vitest.config.ts @@ -0,0 +1,25 @@ +import { resolve } from "node:path"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + resolve: { + alias: { + "@/": resolve(__dirname, "src") + "/", + }, + }, + test: { + globals: true, + environment: "node", + testTimeout: 60000, + hookTimeout: 60000, + include: [ + "src/**/*.{test,spec}.{ts,tsx}", + ], + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/build/**", + "test/**/*.integration.{ts,tsx}", + ], + }, +}); diff --git a/apps/client/src/features/admin/components/AdminLookupTable.tsx b/apps/client/src/features/admin/components/AdminLookupTable.tsx index 3be6091..c073391 100644 --- a/apps/client/src/features/admin/components/AdminLookupTable.tsx +++ b/apps/client/src/features/admin/components/AdminLookupTable.tsx @@ -41,7 +41,7 @@ export const AdminLookupTable = ({ const [editDescription, setEditDescription] = useState(""); const [deleteConfirmId, setDeleteConfirmId] = useState(null); - const queryKey = ["AdminLookupEntities", { entityType, searchTerm }]; + const _queryKey = ["AdminLookupEntities", { entityType, searchTerm }]; const { data, isLoading } = useAdminLookupEntitiesQuery( { @@ -204,9 +204,7 @@ export const AdminLookupTable = ({ ) : ( <> - - {entity.name} - + {entity.name} {entity.description ?? "-"} diff --git a/apps/client/src/features/admin/components/AiCallLogTable.tsx b/apps/client/src/features/admin/components/AiCallLogTable.tsx index ee991da..962883a 100644 --- a/apps/client/src/features/admin/components/AiCallLogTable.tsx +++ b/apps/client/src/features/admin/components/AiCallLogTable.tsx @@ -1,5 +1,5 @@ -import { useState } from "react"; import { Badge, Card, StatusBadge, StatusDot } from "@cv/ui"; +import { useState } from "react"; import type { AiCallLogQuery } from "@/generated/graphql"; type CallLogEntry = AiCallLogQuery["aiCallLog"][number]; @@ -88,7 +88,7 @@ const CallLogRow = ({ {entry.error} ) : ( - entry.finishReason ?? entry.status + (entry.finishReason ?? entry.status) )} @@ -247,9 +247,7 @@ export const AiCallLogTable = ({ entries }: AiCallLogTableProps) => { Time Provider Model - - Duration - + Duration Tokens Status diff --git a/apps/client/src/features/admin/components/QueueMessagesTable.tsx b/apps/client/src/features/admin/components/QueueMessagesTable.tsx index c09a585..8b734b9 100644 --- a/apps/client/src/features/admin/components/QueueMessagesTable.tsx +++ b/apps/client/src/features/admin/components/QueueMessagesTable.tsx @@ -1,9 +1,8 @@ -import { useState } from "react"; import { Badge, Card, StatusDot } from "@cv/ui"; +import { useState } from "react"; import type { QueueMessagesQuery } from "@/generated/graphql"; type MessageConnection = QueueMessagesQuery["queueMessages"]; -type Message = MessageConnection["edges"][number]["node"]; type StatusFilter = "all" | "pending" | "scheduled" | "processing"; @@ -114,14 +113,10 @@ export const QueueMessagesTable = ({ connection }: QueueMessagesTableProps) => { {msg.messageName ?? "-"} - - {msg.queueName} - + {msg.queueName} - + {msg.status} diff --git a/apps/client/src/features/applications/components/ApplicationsOverview.tsx b/apps/client/src/features/applications/components/ApplicationsOverview.tsx index 59e4765..157ebfd 100644 --- a/apps/client/src/features/applications/components/ApplicationsOverview.tsx +++ b/apps/client/src/features/applications/components/ApplicationsOverview.tsx @@ -111,9 +111,9 @@ export const ApplicationsOverview = () => { {application.vacancy?.company?.name} •{" "} {application.vacancy?.role?.name}

- {application.vacancy.location && ( + {application.vacancy.location?.name && (

- 📍 {application.vacancy.location} + 📍 {application.vacancy.location.name}

)} diff --git a/apps/client/src/features/applications/components/MatchIndicator.tsx b/apps/client/src/features/applications/components/MatchIndicator.tsx index 6f6cf47..ab14f08 100644 --- a/apps/client/src/features/applications/components/MatchIndicator.tsx +++ b/apps/client/src/features/applications/components/MatchIndicator.tsx @@ -46,7 +46,8 @@ export const MatchIndicator = ({ // Extract skills from user's job experience const experiences = - jobExperienceData?.profile?.experience?.edges?.map((edge) => edge.node) || []; + jobExperienceData?.profile?.experience?.edges?.map((edge) => edge.node) || + []; const userSkills = experiences.flatMap( (job) => job.skills?.map((skill: { id: string; name: string }) => ({ diff --git a/apps/client/src/features/applications/components/VacancyCard.tsx b/apps/client/src/features/applications/components/VacancyCard.tsx index e866248..ad43772 100644 --- a/apps/client/src/features/applications/components/VacancyCard.tsx +++ b/apps/client/src/features/applications/components/VacancyCard.tsx @@ -17,7 +17,7 @@ interface VacancyCardProps { level?: { name: string; } | null; - location?: string | null; + location?: { name: string } | null; minSalary?: number | null; maxSalary?: number | null; applicationUrl?: string | null; @@ -62,10 +62,10 @@ export const VacancyCard = ({ )} {role?.name && • {role.name}} {level?.name && • {level.name}} - {location && ( + {location?.name && ( - {location} + {location.name} )} {(() => { diff --git a/apps/client/src/features/applications/components/VacancyFilterPanel/useFilterOptions.ts b/apps/client/src/features/applications/components/VacancyFilterPanel/useFilterOptions.ts index c84202b..67eccd7 100644 --- a/apps/client/src/features/applications/components/VacancyFilterPanel/useFilterOptions.ts +++ b/apps/client/src/features/applications/components/VacancyFilterPanel/useFilterOptions.ts @@ -66,8 +66,8 @@ export const useFilterOptions = () => { const locations = Array.from( new Set( vacancies - .map((v) => v.location) - .filter((location): location is string => location !== null), + .map((v) => v.location?.name) + .filter((name): name is string => name != null), ), ); diff --git a/apps/client/src/features/applications/components/VacancySelector.tsx b/apps/client/src/features/applications/components/VacancySelector.tsx index eee6751..de58ccf 100644 --- a/apps/client/src/features/applications/components/VacancySelector.tsx +++ b/apps/client/src/features/applications/components/VacancySelector.tsx @@ -19,7 +19,7 @@ interface Vacancy { level?: { name: string; } | null; - location?: string | null; + location?: { name: string } | null; minSalary?: number | null; maxSalary?: number | null; applicationUrl?: string | null; diff --git a/apps/client/src/features/applications/mutations/create-application.graphql b/apps/client/src/features/applications/mutations/create-application.graphql index 437c65d..bc84d40 100644 --- a/apps/client/src/features/applications/mutations/create-application.graphql +++ b/apps/client/src/features/applications/mutations/create-application.graphql @@ -20,7 +20,11 @@ mutation CreateApplication($input: CreateApplicationInput!) { id title description - location + location { + id + name + type + } minSalary maxSalary company { diff --git a/apps/client/src/features/applications/queries/my-applications.graphql b/apps/client/src/features/applications/queries/my-applications.graphql index 5370773..1491278 100644 --- a/apps/client/src/features/applications/queries/my-applications.graphql +++ b/apps/client/src/features/applications/queries/my-applications.graphql @@ -23,7 +23,11 @@ query MyApplications { id title description - location + location { + id + name + type + } minSalary maxSalary company { diff --git a/apps/client/src/features/onboarding/components/FileUploadStep.tsx b/apps/client/src/features/onboarding/components/FileUploadStep.tsx index 0660b51..3d6c290 100644 --- a/apps/client/src/features/onboarding/components/FileUploadStep.tsx +++ b/apps/client/src/features/onboarding/components/FileUploadStep.tsx @@ -84,7 +84,11 @@ export const FileUploadStep = ({ profileId, onComplete, onBack }: Props) => { uploadMutation.mutate(file, { onSuccess: (result) => { - const { id, profileId: newProfileId, isDuplicate } = result.uploadFile; + const { + id, + profileId: newProfileId, + isDuplicate, + } = result.uploadFile; if (newProfileId) { setActiveProfileId(newProfileId); diff --git a/apps/client/src/features/onboarding/components/ReviewStep.tsx b/apps/client/src/features/onboarding/components/ReviewStep.tsx index f21bb52..c6986d8 100644 --- a/apps/client/src/features/onboarding/components/ReviewStep.tsx +++ b/apps/client/src/features/onboarding/components/ReviewStep.tsx @@ -37,7 +37,9 @@ const collectUnresolved = ( const map = new Map(); entities .filter((e) => !e.id && e.name) - .forEach((e) => map.set(e.name.toLowerCase(), e)); + .forEach((e) => { + map.set(e.name.toLowerCase(), e); + }); return map; }; @@ -104,10 +106,14 @@ export const ReviewStep = ({ const allJobs = parsedData.jobExperiences; const allEdu = parsedData.education; - const unresolvedCompanies = collectUnresolved(allJobs.map((j) => j.company)); + const unresolvedCompanies = collectUnresolved( + allJobs.map((j) => j.company), + ); const unresolvedRoles = collectUnresolved(allJobs.map((j) => j.role)); const unresolvedLevels = collectUnresolved(allJobs.map((j) => j.level)); - const unresolvedInstitutions = collectUnresolved(allEdu.map((e) => e.institution)); + const unresolvedInstitutions = collectUnresolved( + allEdu.map((e) => e.institution), + ); const unresolvedSkills = collectUnresolved([ ...allJobs.flatMap((j) => j.skills), ...allEdu.flatMap((e) => e.skills), @@ -132,11 +138,19 @@ export const ReviewStep = ({ let created = 0; if (totalEntities > 0) { - setProgress({ phase: "Creating entities", current: 0, total: totalEntities }); + setProgress({ + phase: "Creating entities", + current: 0, + total: totalEntities, + }); const bump = () => { created++; - setProgress({ phase: "Creating entities", current: created, total: totalEntities }); + setProgress({ + phase: "Creating entities", + current: created, + total: totalEntities, + }); }; const entityTasks: (() => Promise)[] = []; @@ -144,35 +158,45 @@ export const ReviewStep = ({ unresolvedCompanies.forEach((entity) => { entityTasks.push(async () => { const result = await createCompany({ name: entity.name }); - idMap.get("company")!.set(entity.name.toLowerCase(), result.createCompany.id); + idMap + .get("company") + ?.set(entity.name.toLowerCase(), result.createCompany.id); }); }); unresolvedRoles.forEach((entity) => { entityTasks.push(async () => { const result = await createRole({ name: entity.name }); - idMap.get("role")!.set(entity.name.toLowerCase(), result.createRole.id); + idMap + .get("role") + ?.set(entity.name.toLowerCase(), result.createRole.id); }); }); unresolvedLevels.forEach((entity) => { entityTasks.push(async () => { const result = await createLevel({ name: entity.name }); - idMap.get("level")!.set(entity.name.toLowerCase(), result.createLevel.id); + idMap + .get("level") + ?.set(entity.name.toLowerCase(), result.createLevel.id); }); }); unresolvedInstitutions.forEach((entity) => { entityTasks.push(async () => { const result = await createInstitution({ name: entity.name }); - idMap.get("institution")!.set(entity.name.toLowerCase(), result.createInstitution.id); + idMap + .get("institution") + ?.set(entity.name.toLowerCase(), result.createInstitution.id); }); }); unresolvedSkills.forEach((entity) => { entityTasks.push(async () => { const result = await createSkill({ name: entity.name }); - idMap.get("skill")!.set(entity.name.toLowerCase(), result.createSkill.id); + idMap + .get("skill") + ?.set(entity.name.toLowerCase(), result.createSkill.id); }); }); @@ -183,9 +207,13 @@ export const ReviewStep = ({ if (entityErrors.length > 0) { errors.push( - ...entityErrors.slice(0, 3).map((r) => - r.reason instanceof Error ? r.reason.message : "Entity creation failed", - ), + ...entityErrors + .slice(0, 3) + .map((r) => + r.reason instanceof Error + ? r.reason.message + : "Entity creation failed", + ), ); } } @@ -201,7 +229,11 @@ export const ReviewStep = ({ const recordBump = () => { savedRecords++; - setProgress({ phase: "Saving records", current: savedRecords, total: totalRecords }); + setProgress({ + phase: "Saving records", + current: savedRecords, + total: totalRecords, + }); }; const jobTasks = allJobs.map((job) => async () => { @@ -212,7 +244,7 @@ export const ReviewStep = ({ .map((s) => resolveId("skill", s)) .filter((id): id is string => id !== null); - if (!companyId || !roleId || !levelId) { + if (!(companyId && roleId && levelId)) { throw new Error(`Missing entity IDs for job at ${job.company.name}`); } @@ -262,9 +294,13 @@ export const ReviewStep = ({ if (recordErrors.length > 0) { errors.push( - ...recordErrors.slice(0, 3).map((r) => - r.reason instanceof Error ? r.reason.message : "Record creation failed", - ), + ...recordErrors + .slice(0, 3) + .map((r) => + r.reason instanceof Error + ? r.reason.message + : "Record creation failed", + ), ); } @@ -274,7 +310,9 @@ export const ReviewStep = ({ queryClient.invalidateQueries({ queryKey: ["MeEducation"] }), ]); - const succeeded = recordResults.filter((r) => r.status === "fulfilled").length; + const succeeded = recordResults.filter( + (r) => r.status === "fulfilled", + ).length; if (errors.length > 0) { showError( @@ -434,7 +472,9 @@ export const ReviewStep = ({
diff --git a/apps/client/src/features/profile/components/ProfileSwitcher.tsx b/apps/client/src/features/profile/components/ProfileSwitcher.tsx index a92e57d..19fa0f5 100644 --- a/apps/client/src/features/profile/components/ProfileSwitcher.tsx +++ b/apps/client/src/features/profile/components/ProfileSwitcher.tsx @@ -22,7 +22,7 @@ export const ProfileSwitcher = () => { }; return ( -
+
diff --git a/apps/client/src/features/user-settings/components/AiPreferenceStep.tsx b/apps/client/src/features/user-settings/components/AiPreferenceStep.tsx index 0229632..e311237 100644 --- a/apps/client/src/features/user-settings/components/AiPreferenceStep.tsx +++ b/apps/client/src/features/user-settings/components/AiPreferenceStep.tsx @@ -61,7 +61,10 @@ export const AiPreferenceStep = ({ onComplete }: Props) => {

Add your first AI provider

- setByokProviderAdded(true)} /> + setByokProviderAdded(true)} + />
)} diff --git a/apps/client/src/features/user-settings/components/AiProviderList.tsx b/apps/client/src/features/user-settings/components/AiProviderList.tsx index b180572..148bbcf 100644 --- a/apps/client/src/features/user-settings/components/AiProviderList.tsx +++ b/apps/client/src/features/user-settings/components/AiProviderList.tsx @@ -74,9 +74,7 @@ export const AiProviderList = ({ "Provider removed. Switched back to platform AI since no BYOK keys remain.", ); } else if (removal?.wasActive) { - showSuccess( - "Provider removed. Another key has been set as active.", - ); + showSuccess("Provider removed. Another key has been set as active."); } else { showSuccess("Provider removed"); } diff --git a/apps/client/src/features/user-settings/components/ImportedFilesSection.tsx b/apps/client/src/features/user-settings/components/ImportedFilesSection.tsx index ab6e2e0..66a6ff5 100644 --- a/apps/client/src/features/user-settings/components/ImportedFilesSection.tsx +++ b/apps/client/src/features/user-settings/components/ImportedFilesSection.tsx @@ -67,15 +67,16 @@ export const ImportedFilesSection = () => { } if (files.length === 0) { - return ( -

No imported files yet.

- ); + return

No imported files yet.

; } return (
{files.map((file) => { - const status = STATUS_STYLES[file.status] ?? { dot: "bg-ctp-overlay1", text: "Unknown" }; + const status = STATUS_STYLES[file.status] ?? { + dot: "bg-ctp-overlay1", + text: "Unknown", + }; const sourceLabel = SOURCE_LABELS[file.source] ?? file.source; return ( @@ -84,15 +85,19 @@ export const ImportedFilesSection = () => { className="flex items-center justify-between rounded-lg border border-ctp-surface0 bg-ctp-surface0/30 px-4 py-3" >
-
+

{file.fileName}

- {sourceLabel} · {formatFileSize(file.sizeBytes)} ·{" "} - {formatDate(file.createdAt)} ·{" "} - + {sourceLabel} · {formatFileSize(file.sizeBytes)}{" "} + · {formatDate(file.createdAt)} ·{" "} + {status.text}

diff --git a/apps/client/src/features/vacancies/queries/create-vacancy.graphql b/apps/client/src/features/vacancies/queries/create-vacancy.graphql index 77e5549..b81d73b 100644 --- a/apps/client/src/features/vacancies/queries/create-vacancy.graphql +++ b/apps/client/src/features/vacancies/queries/create-vacancy.graphql @@ -6,7 +6,7 @@ mutation CreateVacancy( $jobTypeId: String $description: String $requirements: String - $location: String + $locationId: String $minSalary: Float $maxSalary: Float $applicationUrl: String @@ -22,7 +22,7 @@ mutation CreateVacancy( jobTypeId: $jobTypeId description: $description requirements: $requirements - location: $location + locationId: $locationId minSalary: $minSalary maxSalary: $maxSalary applicationUrl: $applicationUrl @@ -33,13 +33,8 @@ mutation CreateVacancy( id title ownerId - companyId - roleId - levelId - jobTypeId description requirements - location minSalary maxSalary applicationUrl @@ -48,5 +43,22 @@ mutation CreateVacancy( isPublic createdAt updatedAt + company { + id + name + } + role { + id + name + } + level { + id + name + } + location { + id + name + type + } } } diff --git a/apps/client/src/features/vacancies/queries/my-vacancies.graphql b/apps/client/src/features/vacancies/queries/my-vacancies.graphql index 5a801dc..ad5b2f8 100644 --- a/apps/client/src/features/vacancies/queries/my-vacancies.graphql +++ b/apps/client/src/features/vacancies/queries/my-vacancies.graphql @@ -6,13 +6,8 @@ query MyVacancies($filter: VacancyFilterInput) { id title ownerId - companyId - roleId - levelId - jobTypeId description requirements - location minSalary maxSalary applicationUrl @@ -33,6 +28,19 @@ query MyVacancies($filter: VacancyFilterInput) { id name } + location { + id + name + type + } + contractType { + id + name + } + rateType { + id + name + } skills { id name diff --git a/apps/client/src/features/vacancies/queries/vacancy-filter-options.graphql b/apps/client/src/features/vacancies/queries/vacancy-filter-options.graphql index 2f52c78..668a8e0 100644 --- a/apps/client/src/features/vacancies/queries/vacancy-filter-options.graphql +++ b/apps/client/src/features/vacancies/queries/vacancy-filter-options.graphql @@ -12,7 +12,11 @@ query VacancyFilterOptions { id name } - location + location { + id + name + type + } } } } diff --git a/apps/client/src/pages/AdminPage.tsx b/apps/client/src/pages/AdminPage.tsx index c01802f..d0ac9f0 100644 --- a/apps/client/src/pages/AdminPage.tsx +++ b/apps/client/src/pages/AdminPage.tsx @@ -15,7 +15,9 @@ const ENTITY_TABS = [ ] as const; export default function AdminPage() { - const [activeTab, setActiveTab] = useState<(typeof ENTITY_TABS)[number]["type"]>(ENTITY_TABS[0].type); + const [activeTab, setActiveTab] = useState< + (typeof ENTITY_TABS)[number]["type"] + >(ENTITY_TABS[0].type); const activeEntry = ENTITY_TABS.find((t) => t.type === activeTab); diff --git a/apps/client/src/pages/CVViewPage.tsx b/apps/client/src/pages/CVViewPage.tsx index 6513766..2e32970 100644 --- a/apps/client/src/pages/CVViewPage.tsx +++ b/apps/client/src/pages/CVViewPage.tsx @@ -150,10 +150,16 @@ export const CVViewPage = () => { - + Edit CV - + Back to List
@@ -178,7 +184,8 @@ export const CVViewPage = () => { /> ) : (
- No template content available. The selected template may not have a body defined. + No template content available. The selected template may not have + a body defined.
)}
diff --git a/apps/client/src/pages/DashboardPage.tsx b/apps/client/src/pages/DashboardPage.tsx index b4aeae4..6d99213 100644 --- a/apps/client/src/pages/DashboardPage.tsx +++ b/apps/client/src/pages/DashboardPage.tsx @@ -40,8 +40,7 @@ export default function DashboardPage() { } const user = userData?.me; - const allCvs = - cvsData?.profile?.cvs?.edges?.map((e) => e.node) ?? []; + const allCvs = cvsData?.profile?.cvs?.edges?.map((e) => e.node) ?? []; const totalCVs = cvsData?.profile?.cvs?.totalCount ?? 0; const totalTemplates = templatesData?.cvTemplates?.totalCount || 0; diff --git a/apps/client/src/pages/EducationPage.tsx b/apps/client/src/pages/EducationPage.tsx index 4a7237e..c043d53 100644 --- a/apps/client/src/pages/EducationPage.tsx +++ b/apps/client/src/pages/EducationPage.tsx @@ -7,7 +7,12 @@ import { useMeEducationQuery } from "@/generated/graphql"; export default function EducationPage() { const profileId = useActiveProfileId(); - const { data, isPending: loading, error, refetch } = useMeEducationQuery({ profileId }); + const { + data, + isPending: loading, + error, + refetch, + } = useMeEducationQuery({ profileId }); const { showError } = useToast(); const navigate = useNavigate(); diff --git a/apps/client/src/pages/ProfilePage.tsx b/apps/client/src/pages/ProfilePage.tsx index 9845c38..3d2e913 100644 --- a/apps/client/src/pages/ProfilePage.tsx +++ b/apps/client/src/pages/ProfilePage.tsx @@ -26,8 +26,6 @@ export default function ProfilePage() { phone: activeProfile.phone ?? "", address: activeProfile.address ?? "", postalCode: activeProfile.postalCode ?? "", - city: activeProfile.city ?? "", - country: activeProfile.country ?? "", website: activeProfile.website ?? "", linkedInUrl: activeProfile.linkedInUrl ?? "", summary: activeProfile.summary ?? "", diff --git a/apps/client/src/pages/ProfilesPage.tsx b/apps/client/src/pages/ProfilesPage.tsx index c80295d..703a4ef 100644 --- a/apps/client/src/pages/ProfilesPage.tsx +++ b/apps/client/src/pages/ProfilesPage.tsx @@ -16,8 +16,7 @@ const formatDate = (dateStr: string): string => }); export default function ProfilesPage() { - const { profiles, activeProfile, setActiveProfileId, refetch } = - useProfile(); + const { profiles, activeProfile, setActiveProfileId, refetch } = useProfile(); const { showSuccess, showError } = useToast(); const { showConfirmation } = useConfirmationModal(); const queryClient = useQueryClient(); @@ -38,9 +37,7 @@ export default function ProfilesPage() { showSuccess("Profile created"); navigate("/profile"); } catch (err) { - showError( - extractGraphQLErrorMessage(err) || "Failed to create profile", - ); + showError(extractGraphQLErrorMessage(err) || "Failed to create profile"); } }; @@ -66,9 +63,7 @@ export default function ProfilesPage() { await queryClient.invalidateQueries(); showSuccess("Profile deleted"); } catch (err) { - showError( - extractGraphQLErrorMessage(err) || "Failed to delete profile", - ); + showError(extractGraphQLErrorMessage(err) || "Failed to delete profile"); } }; @@ -116,12 +111,6 @@ export default function ProfilesPage() { )}
- {(profile.city || profile.country) && ( -

- {[profile.city, profile.country].filter(Boolean).join(", ")} -

- )} -

Created {formatDate(profile.createdAt)}

@@ -150,7 +139,9 @@ export default function ProfilesPage() { size="sm" variant="ghost" onClick={() => handleDelete(profile.id, profile.name)} - disabled={profiles.length <= 1 || deleteProfileMutation.isPending} + disabled={ + profiles.length <= 1 || deleteProfileMutation.isPending + } > Delete diff --git a/apps/client/src/pages/SettingsPage.tsx b/apps/client/src/pages/SettingsPage.tsx index d5830a8..dc9b0ee 100644 --- a/apps/client/src/pages/SettingsPage.tsx +++ b/apps/client/src/pages/SettingsPage.tsx @@ -138,9 +138,7 @@ export default function SettingsPage() {

{user?.email}

-
- User ID -
+
User ID

{user?.id}

diff --git a/apps/client/src/pages/SystemStatusPage.tsx b/apps/client/src/pages/SystemStatusPage.tsx index 39c7e18..51c3e6f 100644 --- a/apps/client/src/pages/SystemStatusPage.tsx +++ b/apps/client/src/pages/SystemStatusPage.tsx @@ -1,4 +1,8 @@ import { Badge, Card, PageHeader, Placeholder, StatusDot } from "@cv/ui"; +import { AiCallLogTable } from "@/features/admin/components/AiCallLogTable"; +import { QueueMessagesTable } from "@/features/admin/components/QueueMessagesTable"; +import { QueueStatsCard } from "@/features/admin/components/QueueStatsCard"; +import { WorkerHealthCard } from "@/features/admin/components/WorkerHealthCard"; import { useAiCallLogQuery, useQueueMessagesQuery, @@ -6,10 +10,6 @@ import { useSystemStatusQuery, useWorkerHealthQuery, } from "@/generated/graphql"; -import { AiCallLogTable } from "@/features/admin/components/AiCallLogTable"; -import { QueueMessagesTable } from "@/features/admin/components/QueueMessagesTable"; -import { QueueStatsCard } from "@/features/admin/components/QueueStatsCard"; -import { WorkerHealthCard } from "@/features/admin/components/WorkerHealthCard"; interface LlamaCppDetails { kind: "llama-cpp"; @@ -124,7 +124,9 @@ const ProviderCard = ({ {details?.kind === "api" && ( )} - {details?.baseUrl && } + {details?.baseUrl && ( + + )}
); @@ -298,9 +300,7 @@ export default function SystemStatusPage() { {details?.kind === "llama-cpp" && details.slots && - Array.isArray(details.slots) && ( - - )} + Array.isArray(details.slots) && } {queueStatsData?.queueStats && ( diff --git a/apps/client/src/pages/VacanciesPage.tsx b/apps/client/src/pages/VacanciesPage.tsx index 9d1c09e..80898dc 100644 --- a/apps/client/src/pages/VacanciesPage.tsx +++ b/apps/client/src/pages/VacanciesPage.tsx @@ -87,7 +87,7 @@ export default function VacanciesPage() { }, description: v.description, requirements: v.requirements, - location: v.location, + location: v.location?.name, minSalary: v.minSalary, maxSalary: v.maxSalary, jobType: null, diff --git a/apps/docs/package.json b/apps/docs/package.json index 8514c44..405ddd0 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -39,6 +39,7 @@ "postcss": "^8.4.47", "tailwindcss": "^4.0.0", "typescript": "^5.5.3", + "unified": "^11.0.5", "vite": "^5.4.3" } } diff --git a/apps/docs/src/config/env-interpolation-remark-plugin.ts b/apps/docs/src/config/env-interpolation-remark-plugin.ts index 5fe29b7..2e90303 100644 --- a/apps/docs/src/config/env-interpolation-remark-plugin.ts +++ b/apps/docs/src/config/env-interpolation-remark-plugin.ts @@ -3,9 +3,14 @@ import type { Plugin } from "unified"; import { ENV_VARS } from "./env.config"; const visitTextNodes = (node: Node, fn: (text: Text) => void): void => { - if (node.type === "text") return fn(node as Text); + if (node.type === "text") { + fn(node as Text); + return; + } if ("children" in node) - (node as Root).children.forEach((child) => visitTextNodes(child, fn)); + (node as Root).children.forEach((child) => { + visitTextNodes(child, fn); + }); }; /** diff --git a/apps/server/jest.config.ts b/apps/server/jest.config.ts deleted file mode 100644 index da145f2..0000000 --- a/apps/server/jest.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Config } from "jest"; - -const config: Config = { - preset: "ts-jest", - testEnvironment: "node", - rootDir: ".", - moduleFileExtensions: ["ts", "tsx", "js", "json"], - testMatch: [ - "/src/**/*.spec.ts", - "/src/**/*.test.ts", - "/src/**/*.spec.tsx", - "/src/**/*.test.tsx", - ], - transform: { - "^.+\\.(t|j)sx?$": [ - "ts-jest", - { - diagnostics: false, - tsconfig: { - sourceMap: true, - inlineSourceMap: true, - }, - }, - ], - }, - moduleNameMapper: { - "^@/(.*)$": "/src/$1", - }, - collectCoverage: true, - collectCoverageFrom: [ - "/src/**/*.{ts,tsx}", - "!/src/main.ts", - "!/src/**/index.ts", - "!/src/**/__mocks__/**", - ], - coverageDirectory: "/coverage-unit", - coverageReporters: ["text", "lcov"], - coverageProvider: "babel", -}; - -export default config; diff --git a/apps/server/prisma/models/vacancy.prisma b/apps/server/prisma/models/vacancy.prisma deleted file mode 100644 index dc23395..0000000 --- a/apps/server/prisma/models/vacancy.prisma +++ /dev/null @@ -1,80 +0,0 @@ -model JobType { - id String @id @default(cuid()) - name String @unique - description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Vacancies with this job type - vacancies Vacancy[] - - @@map("job_types") -} - -model ApplicationStatus { - id String @id @default(cuid()) - name String @unique - description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Applications with this status - applications Application[] - - @@map("application_statuses") -} - -model Vacancy { - id String @id @default(cuid()) - ownerId String - title String - companyId String - roleId String - levelId String? - jobTypeId String? - description String? - requirements String? - location String? - minSalary Int? - maxSalary Int? - applicationUrl String? - deadline DateTime? - isActive Boolean @default(true) - isPublic Boolean @default(false) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations - owner User @relation("VacancyOwner", fields: [ownerId], references: [id], onDelete: Cascade) - company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) - role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) - level Level? @relation(fields: [levelId], references: [id], onDelete: Cascade) - jobType JobType? @relation(fields: [jobTypeId], references: [id], onDelete: Cascade) - skills Skill[] - applications Application[] - - @@map("vacancies") -} - -model Application { - id String @id @default(cuid()) - userId String - profileId String - vacancyId String - cvId String? - coverLetter String? - statusId String - appliedAt DateTime @default(now()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) - vacancy Vacancy @relation(fields: [vacancyId], references: [id], onDelete: Cascade) - cv CV? @relation(fields: [cvId], references: [id], onDelete: SetNull) - status ApplicationStatus @relation(fields: [statusId], references: [id], onDelete: Restrict) - - @@unique([userId, vacancyId]) - @@map("applications") -} diff --git a/apps/server/src/modules/data-import/events/user-file-created.event.ts b/apps/server/src/modules/data-import/events/user-file-created.event.ts deleted file mode 100644 index 57fdbb6..0000000 --- a/apps/server/src/modules/data-import/events/user-file-created.event.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { User } from "@cv/auth"; -import type { DataImportSource } from "../data-import-source.interface"; - -export class UserFileCreatedEvent { - static readonly event = "user-file.created"; - - constructor( - public readonly userFileId: string, - public readonly user: User, - public readonly source: DataImportSource, - public readonly params: Record, - ) {} -} diff --git a/apps/server/src/modules/vacancies/graphql/vacancy.type.ts b/apps/server/src/modules/vacancies/graphql/vacancy.type.ts deleted file mode 100644 index d419715..0000000 --- a/apps/server/src/modules/vacancies/graphql/vacancy.type.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Field, ID, ObjectType } from "@nestjs/graphql"; -import { createConnection } from "@/modules/base/connection.factory"; -import { Company } from "@/modules/job-experience/company/graphql/company.type"; -import { Level } from "@/modules/job-experience/level/graphql/level.type"; -import { Role } from "@/modules/job-experience/role/graphql/role.type"; -import type { Vacancy as DomainVacancy } from "@/modules/vacancies/vacancy.entity"; -import { Vacancy as VacancyEntity } from "@/modules/vacancies/vacancy.entity"; - -@ObjectType() -export class Vacancy { - @Field(() => ID) - id: string; - - @Field(() => String) - title: string; - - @Field(() => String) - ownerId: string; - - @Field(() => String) - companyId: string; - - @Field(() => String) - roleId: string; - - @Field(() => String, { nullable: true }) - levelId: string | null; - - @Field(() => Company) - company?: Company; - - @Field(() => Role) - role?: Role; - - @Field(() => Level, { nullable: true }) - level?: Level | null; - - @Field(() => String, { nullable: true }) - jobTypeId: string | null; - - @Field(() => String, { nullable: true }) - description: string | null; - - @Field(() => String, { nullable: true }) - requirements: string | null; - - @Field(() => String, { nullable: true }) - location: string | null; - - @Field(() => Number, { nullable: true }) - minSalary: number | null; - - @Field(() => Number, { nullable: true }) - maxSalary: number | null; - - @Field(() => String, { nullable: true }) - applicationUrl: string | null; - - @Field(() => Date, { nullable: true }) - deadline: Date | null; - - @Field(() => Boolean) - isActive: boolean; - - @Field(() => Boolean) - isPublic: boolean; - - @Field(() => Date) - createdAt: Date; - - @Field(() => Date) - updatedAt: Date; - - constructor(data: { - id: string; - title: string; - ownerId: string; - companyId: string; - roleId: string; - levelId?: string | null; - jobTypeId?: string | null; - description?: string | null; - requirements?: string | null; - location?: string | null; - minSalary?: number | null; - maxSalary?: number | null; - applicationUrl?: string | null; - deadline?: Date | null; - isActive: boolean; - isPublic: boolean; - createdAt: Date; - updatedAt: Date; - }) { - this.id = data.id; - this.title = data.title; - this.ownerId = data.ownerId; - this.companyId = data.companyId; - this.roleId = data.roleId; - this.levelId = data.levelId ?? null; - this.jobTypeId = data.jobTypeId ?? null; - this.description = data.description ?? null; - this.requirements = data.requirements ?? null; - this.location = data.location ?? null; - this.minSalary = data.minSalary ?? null; - this.maxSalary = data.maxSalary ?? null; - this.applicationUrl = data.applicationUrl ?? null; - this.deadline = data.deadline ?? null; - this.isActive = data.isActive; - this.isPublic = data.isPublic; - this.createdAt = data.createdAt; - this.updatedAt = data.updatedAt; - } - - static fromDomain(domainVacancy: DomainVacancy): Vacancy { - return new Vacancy({ - id: domainVacancy.id, - title: domainVacancy.title, - ownerId: domainVacancy.ownerId, - companyId: domainVacancy.company.id, - roleId: domainVacancy.role.id, - levelId: domainVacancy.level?.id ?? null, - jobTypeId: domainVacancy.jobTypeId ?? null, - description: domainVacancy.description ?? null, - requirements: domainVacancy.requirements ?? null, - location: domainVacancy.location ?? null, - minSalary: domainVacancy.minSalary ?? null, - maxSalary: domainVacancy.maxSalary ?? null, - applicationUrl: domainVacancy.applicationUrl ?? null, - deadline: domainVacancy.deadline ?? null, - isActive: domainVacancy.isActive, - isPublic: domainVacancy.isPublic, - createdAt: domainVacancy.createdAt, - updatedAt: domainVacancy.updatedAt, - }); - } -} - -export const { Connection: VacancyConnection, Edge: VacancyEdge } = - createConnection(Vacancy, (domain) => - Vacancy.fromDomain(domain), - ); - -export type VacancyConnection = InstanceType; -export type VacancyEdge = InstanceType; diff --git a/apps/server/test/coverage-unit/lcov-report/base.css b/apps/server/test/coverage-unit/lcov-report/base.css deleted file mode 100644 index f418035..0000000 --- a/apps/server/test/coverage-unit/lcov-report/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/apps/server/test/coverage-unit/lcov-report/favicon.png b/apps/server/test/coverage-unit/lcov-report/favicon.png deleted file mode 100644 index c1525b811a167671e9de1fa78aab9f5c0b61cef7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 445 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4kiW$h6xih%orFLTRdGHLn;`zo@efna1>#C zP+yQ1a@(xwR@vcOg|93^@4RTb&rJ?ebaY4}0YgS)l{}^%au- z_;}9Zak48mJg9F`WyPkDf8Sd#=J(R3x$KX>%X`|@oLYbHga7uVXX@u){?wjyMnO+1 zTuHI+XIsFTuRkt*ZISfi{Q1E1(+AH@-!uMvG(H%&t*H1$^$z2LHOBP|-?Q^PxVHDn zE8Sgx8|3U1=T6uDT=Zi0;l^IynQwJtgb(g2vAt1la&Mp7X_vDT_Q)j8Yy0|>yZ&F7 z>gzqTLa*+5sbwwg_2_Z>x$}aP)2jaN%AU;Neb~!aH0t4llQ(#3f3m3>T?$;c<;CUD z*(?#4ax%qTk1L*g*`0EZDPVC|MvOnx2~Y9;F|DGdfE4vw1A9SNBas(w7%RP$}py8DW05-uwO!uq6UDXGquW&3{VYqqD7g#7*w zi_^dSQSIK!D!Gi|=%q_%GNyQ3 - - - - Code coverage report for All files - - - - - - - - - -
-
-

All files

-
- -
- Unknown% - Statements - 0/0 -
- - -
- Unknown% - Branches - 0/0 -
- - -
- Unknown% - Functions - 0/0 -
- - -
- Unknown% - Lines - 0/0 -
- - -
-

- Press n or j to go to the next uncovered block, b, p or k for the previous block. -

- -
-
-
- - - - - - - - - - - - - - - - -
FileStatementsBranchesFunctionsLines
-
-
-
- - - - - - - - \ No newline at end of file diff --git a/apps/server/test/coverage-unit/lcov-report/prettify.css b/apps/server/test/coverage-unit/lcov-report/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/apps/server/test/coverage-unit/lcov-report/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/apps/server/test/coverage-unit/lcov-report/sort-arrow-sprite.png b/apps/server/test/coverage-unit/lcov-report/sort-arrow-sprite.png deleted file mode 100644 index 6ed68316eb3f65dec9063332d2f69bf3093bbfab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 138 zcmeAS@N?(olHy`uVBq!ia0y~yU|?rpV36iuV_;y|;5OBmfq@~=)5S5Qf-$*3|3hQ4 zf`Qw!#U2L?e0W@$Vi_Zt#MxdR>HO#=WF|J7#c{m=56hexM(h6^Z(XQmpqg+jN#nrN tssE?!n)YuJ%U&k~fdV%9gAMEqevg;zSU<7i3IhWJgQu&X%Q~loCIA|(EZhJ9 diff --git a/apps/server/test/coverage-unit/lcov.info b/apps/server/test/coverage-unit/lcov.info deleted file mode 100644 index e69de29..0000000 diff --git a/apps/server/test/jest-e2e.json b/apps/server/test/jest-e2e.json deleted file mode 100644 index 0e9d611..0000000 --- a/apps/server/test/jest-e2e.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "preset": "ts-jest", - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".e2e-spec.ts$", - "transform": { - "^.+\\.(t|j)s$": [ - "ts-jest", - { - "diagnostics": false, - "tsconfig": { - "sourceMap": true, - "inlineSourceMap": true - } - } - ] - }, - "moduleNameMapper": { - "^@/(.*)$": "/../src/$1" - }, - "transformIgnorePatterns": ["node_modules/(?!(@faker-js|@cv)/)"], - "collectCoverage": true, - "collectCoverageFrom": [ - "/../src/**/*.{ts,tsx}", - "!/../src/main.ts" - ], - "coverageDirectory": "/coverage", - "coverageReporters": ["text", "lcov"], - "coverageProvider": "babel", - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/../src/.*/index\\.ts$", - "/../src/.*/__mocks__/" - ] -} diff --git a/apps/server/test/jest-unit.json b/apps/server/test/jest-unit.json deleted file mode 100644 index 8c5d726..0000000 --- a/apps/server/test/jest-unit.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "preset": "ts-jest", - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", - "testEnvironment": "node", - "testRegex": ".*([-.])?spec\\.ts$", - "transform": { - "^.+\\.(t|j)s$": [ - "ts-jest", - { - "diagnostics": false, - "tsconfig": { - "sourceMap": true, - "inlineSourceMap": true - } - } - ] - }, - "moduleNameMapper": { - "^@/(.*)$": "/../src/$1" - }, - "transformIgnorePatterns": ["node_modules/(?!(@faker-js|@cv)/)"], - "collectCoverage": true, - "collectCoverageFrom": [ - "/../src/**/*.{ts,tsx}", - "!/../src/main.ts", - "!/../src/**/index.ts", - "!/../src/**/__mocks__/**" - ], - "coverageDirectory": "/coverage-unit", - "coverageReporters": ["text", "lcov"], - "coverageProvider": "babel" -} diff --git a/apps/server/vitest.config.ts b/apps/server/vitest.config.ts deleted file mode 100644 index f1c90ca..0000000 --- a/apps/server/vitest.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defineConfig } from "vitest/config"; - -export default defineConfig({ - test: { - globals: true, - environment: "node", - testTimeout: 60000, // 60 seconds default - hookTimeout: 60000, - include: ["**/*.{test,spec,integration}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"], - exclude: [ - "**/node_modules/**", - "**/dist/**", - "**/build/**", - "**/__tests__/**/*.spec.ts", - ], // Exclude Jest tests - }, -}); diff --git a/apps/worker/package.json b/apps/worker/package.json index 063c5cd..342b109 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -12,11 +12,12 @@ "lint:fix": "biome check --write ." }, "dependencies": { + "@cv/core": "workspace:*", "@cv/file-storage": "*", - "@cv/system": "*", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", + "@cv/handlers": "workspace:*", + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18", "@nestjs/event-emitter": "^3.0.1", "@riotbyte-com/nest-service-locator": "0.3.0-rc.4", "@riotbyte-com/project-q-core": "1.0.19-rc.3", @@ -31,7 +32,9 @@ "zod": "^4.3.6" }, "devDependencies": { + "@aws-sdk/client-s3": "^3.800.0", "@cv/tsconfig": "*", + "@types/pg": "^8.15.0", "@swc/core": "^1.15.13", "@types/node": "^22.7.5", "nodemon": "^3.1.7", diff --git a/apps/worker/src/config.ts b/apps/worker/src/config.ts index 05000da..c88374c 100644 --- a/apps/worker/src/config.ts +++ b/apps/worker/src/config.ts @@ -20,11 +20,15 @@ export const config = { heartbeatFilePath: process.env["HEARTBEAT_FILE_PATH"] ?? "/tmp/worker-heartbeat", heartbeatDbIntervalMs: Number(process.env["HEARTBEAT_DB_INTERVAL_MS"] ?? "0"), - chromiumArgs: (process.env["CHROMIUM_ARGS"] ?? "--no-sandbox,--disable-dev-shm-usage") + chromiumArgs: ( + process.env["CHROMIUM_ARGS"] ?? "--no-sandbox,--disable-dev-shm-usage" + ) .split(",") .map((s) => s.trim()) .filter(Boolean), - fileStorageDriver: (process.env["FILE_STORAGE_DRIVER"] ?? "disk") as "disk" | "r2", + fileStorageDriver: (process.env["FILE_STORAGE_DRIVER"] ?? "disk") as + | "disk" + | "r2", r2Endpoint: process.env["R2_ENDPOINT"] ?? "", r2AccessKeyId: process.env["R2_ACCESS_KEY_ID"] ?? "", r2SecretAccessKey: process.env["R2_SECRET_ACCESS_KEY"] ?? "", diff --git a/apps/worker/src/heartbeat/heartbeat.listener.ts b/apps/worker/src/heartbeat/heartbeat.listener.ts index d8d669c..85d13aa 100644 --- a/apps/worker/src/heartbeat/heartbeat.listener.ts +++ b/apps/worker/src/heartbeat/heartbeat.listener.ts @@ -17,9 +17,14 @@ export class HeartbeatListener implements OnModuleInit { ) {} onModuleInit(): void { - this.emitter.on(WorkerStartedEvent.name, async (event: WorkerStartedEvent) => { - await Promise.all(this.strategies.map((s) => s.onStart(event.worker.id))); - }); + this.emitter.on( + WorkerStartedEvent.name, + async (event: WorkerStartedEvent) => { + await Promise.all( + this.strategies.map((s) => s.onStart(event.worker.id)), + ); + }, + ); this.emitter.on(WorkerRunningEvent.name, async () => { await Promise.all(this.strategies.map((s) => s.onRunning())); diff --git a/apps/worker/src/worker.module.ts b/apps/worker/src/worker.module.ts index 8d9845c..c47a7d7 100644 --- a/apps/worker/src/worker.module.ts +++ b/apps/worker/src/worker.module.ts @@ -1,16 +1,25 @@ +import { DatabaseModule, PrismaService } from "@cv/core"; +import { FileStorageModule } from "@cv/file-storage"; +import { + HandlersModule, + HtmlToPdfService, + RenderPdfHandler, +} from "@cv/handlers"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { EventEmitterModule } from "@nestjs/event-emitter"; -import { DatabaseModule, PrismaService } from "@cv/system"; -import { FileStorageModule } from "@cv/file-storage"; -import { LoggerProvider, MessengerModule } from "@riotbyte-com/project-q-nestjs"; -import { PrismaClientToken, PrismaTransportFactory } from "@riotbyte-com/project-q-prisma"; +import { + LoggerProvider, + MessengerModule, +} from "@riotbyte-com/project-q-nestjs"; +import { + PrismaClientToken, + PrismaTransportFactory, +} from "@riotbyte-com/project-q-prisma"; import type { WorkerConfig } from "./config"; import { config } from "./config"; -import { RenderPdfHandler } from "./handlers/render-pdf.handler"; import { HeartbeatModule } from "./heartbeat/heartbeat.module"; import { NestProjectQLogger } from "./logger.provider"; -import { HtmlToPdfService } from "./pdf/html-to-pdf.service"; export const WORKER_CONFIG = Symbol("WORKER_CONFIG"); @@ -22,6 +31,7 @@ export const WORKER_CONFIG = Symbol("WORKER_CONFIG"); }), DatabaseModule, EventEmitterModule.forRoot(), + HandlersModule, HeartbeatModule.register(config), FileStorageModule.forRoot( config.fileStorageDriver === "r2" @@ -46,8 +56,6 @@ export const WORKER_CONFIG = Symbol("WORKER_CONFIG"); { provide: PrismaClientToken, useExisting: PrismaService }, { provide: LoggerProvider, useClass: NestProjectQLogger }, PrismaTransportFactory, - RenderPdfHandler, - HtmlToPdfService, ], }) export class WorkerModule {} diff --git a/apps/worker/tsconfig.json b/apps/worker/tsconfig.json index 33d1d3b..1c092e3 100644 --- a/apps/worker/tsconfig.json +++ b/apps/worker/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../packages/tsconfig/tsconfig.node.json", "compilerOptions": { "outDir": "dist", - "preserveSymlinks": true, + "preserveSymlinks": false, "types": ["node"] }, "include": ["src/**/*.ts"], diff --git a/ci/deploy.sh b/ci/deploy.sh index ae0739e..3de98e7 100755 --- a/ci/deploy.sh +++ b/ci/deploy.sh @@ -38,14 +38,14 @@ echo "==> Building images" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" build echo "==> Running database migrations" -docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" run --rm server \ - sh -c "cd /app/apps/server && npx prisma migrate deploy" +docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" run --rm api \ + sh -c "cd /app/packages/core && npx prisma migrate deploy" echo "==> Starting services" docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" up -d echo "==> Seeding database (idempotent)" -docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec server \ +docker compose -f "$COMPOSE_FILE" --env-file "$ENV_FILE" exec api \ node dist/scripts/seed.js echo "==> Cleaning up old images" diff --git a/docker-compose.yml b/docker-compose.yml index 8d6d803..37be049 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,10 +15,10 @@ services: timeout: 5s retries: 3 - server: + api: build: context: . - dockerfile: .docker/server.Dockerfile + dockerfile: .docker/api.Dockerfile target: development additional_contexts: project-q: ${PROJECT_Q_PATH:?Set PROJECT_Q_PATH to your local project-q checkout} @@ -44,16 +44,16 @@ services: ports: - "${SERVER_PORT:-3000}:3000" volumes: - - ./apps/server/src:/app/apps/server/src - - ./apps/server/prisma:/app/apps/server/prisma + - ./apps/api/src:/app/apps/api/src + - ./apps/api/prisma:/app/apps/api/prisma - ./packages/ai-parser/src:/app/packages/ai-parser/src - ./packages/ai-provider/src:/app/packages/ai-provider/src - - ./packages/auth/src:/app/packages/auth/src + - ./packages/core/src:/app/packages/core/src - ./packages/cv-renderer/src:/app/packages/cv-renderer/src - ./packages/file-upload/src:/app/packages/file-upload/src - - ./packages/system/src:/app/packages/system/src + - ./packages/core/src:/app/packages/core/src - worker-output:/app/pdf-output:ro - command: sh -c "cd /app/apps/server && pnpm prisma generate && pnpm prisma:deploy && pnpm dev" + command: sh -c "cd /app/apps/api && pnpm prisma generate && pnpm prisma:deploy && pnpm dev" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 15s @@ -68,11 +68,11 @@ services: target: development environment: VITE_SERVER_URL: ${VITE_SERVER_URL:-http://localhost:3000} - VITE_PROXY_TARGET: http://server:3000 + VITE_PROXY_TARGET: http://api:3000 VITE_DOCS_URL: ${VITE_DOCS_URL:-http://localhost:3001} - GRAPHQL_SCHEMA_URL: ${GRAPHQL_SCHEMA_URL:-http://server:3000/graphql} + GRAPHQL_SCHEMA_URL: ${GRAPHQL_SCHEMA_URL:-http://api:3000/graphql} depends_on: - server: + api: condition: service_healthy ports: - "${CLIENT_PORT:-5173}:5173" @@ -80,7 +80,7 @@ services: - ./apps/client/src:/app/apps/client/src - ./packages/routing/src:/app/packages/routing/src - ./packages/ui/src:/app/packages/ui/src - - ./packages/system/src:/app/packages/system/src + - ./packages/core/src:/app/packages/core/src command: sh -c "cd /app/apps/client && (pnpm codegen || true) && pnpm dev" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5173"] @@ -133,7 +133,7 @@ services: - ./apps/docs/content:/app/apps/docs/content - ./packages/routing/src:/app/packages/routing/src - ./packages/ui/src:/app/packages/ui/src - - ./packages/system/src:/app/packages/system/src + - ./packages/core/src:/app/packages/core/src healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3001"] interval: 15s diff --git a/packages/ai-parser/jest.config.ts b/packages/ai-parser/jest.config.ts deleted file mode 100644 index 7535977..0000000 --- a/packages/ai-parser/jest.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Config } from "jest"; - -const config: Config = { - preset: "ts-jest", - testEnvironment: "node", - rootDir: ".", - moduleFileExtensions: ["ts", "tsx", "js", "json"], - testMatch: ["/src/**/*.spec.ts", "/src/**/*.test.ts"], - transform: { - "^.+\\.(t|j)sx?$": [ - "ts-jest", - { - diagnostics: false, - tsconfig: { - sourceMap: true, - inlineSourceMap: true, - }, - }, - ], - }, - collectCoverage: true, - collectCoverageFrom: [ - "/src/**/*.ts", - "!/src/**/index.ts", - "!/src/**/__tests__/**", - ], - coverageDirectory: "/coverage", - coverageReporters: ["text", "lcov"], -}; - -export default config; diff --git a/packages/ai-parser/package.json b/packages/ai-parser/package.json index d7ef9fe..47a5ae8 100644 --- a/packages/ai-parser/package.json +++ b/packages/ai-parser/package.json @@ -6,9 +6,9 @@ "types": "./src/index.ts", "scripts": { "typecheck": "tsc --noEmit", - "test": "jest", - "test:watch": "jest --watch", - "test:coverage": "jest --coverage" + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@cv/ai-provider": "*", @@ -20,10 +20,8 @@ }, "devDependencies": { "@cv/tsconfig": "*", - "@types/jest": "^29.5.0", "@types/node": "^22.7.5", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^4.0.0" } } diff --git a/packages/ai-parser/src/__tests__/ai-parser.service.spec.ts b/packages/ai-parser/src/__tests__/ai-parser.service.spec.ts index 6d25829..e05ddaf 100644 --- a/packages/ai-parser/src/__tests__/ai-parser.service.spec.ts +++ b/packages/ai-parser/src/__tests__/ai-parser.service.spec.ts @@ -1,10 +1,11 @@ import type { AIProvider, AICompletionResponse } from "@cv/ai-provider"; +import type { Mocked } from "vitest"; import { CVParserService } from "../ai-parser.service"; import type { ParsedCVData } from "../schemas"; describe("CVParserService", () => { let service: CVParserService; - let mockProvider: jest.Mocked; + let mockProvider: Mocked; const validParsedCVData: ParsedCVData = { personalInfo: { @@ -45,8 +46,8 @@ describe("CVParserService", () => { beforeEach(() => { mockProvider = { name: "mock-provider", - complete: jest.fn(), - isHealthy: jest.fn().mockResolvedValue(true), + complete: vi.fn(), + isHealthy: vi.fn().mockResolvedValue(true), }; service = new CVParserService(mockProvider); diff --git a/packages/ai-parser/src/ai-parser.service.test.ts b/packages/ai-parser/src/ai-parser.service.test.ts index d4015bf..fdb0581 100644 --- a/packages/ai-parser/src/ai-parser.service.test.ts +++ b/packages/ai-parser/src/ai-parser.service.test.ts @@ -1,4 +1,3 @@ -import { describe, it, expect, jest } from '@jest/globals'; import type { AIProvider, AICompletionResponse } from '@cv/ai-provider'; import { CVParserService } from './ai-parser.service'; @@ -7,12 +6,12 @@ describe('CVParserService Unit Tests', () => { mockResponse: string ): AIProvider => ({ name: 'mock', - complete: jest.fn(async () => ({ + complete: vi.fn(async () => ({ content: mockResponse, model: 'mock-model', finishReason: 'stop', } as AICompletionResponse)), - isHealthy: jest.fn(async () => true), + isHealthy: vi.fn(async () => true), }); describe('JSON Extraction', () => { diff --git a/packages/ai-parser/vitest.config.ts b/packages/ai-parser/vitest.config.ts new file mode 100644 index 0000000..5751952 --- /dev/null +++ b/packages/ai-parser/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + root: "./src", + globals: true, + }, +}); diff --git a/packages/ai-provider/jest.config.ts b/packages/ai-provider/jest.config.ts deleted file mode 100644 index 4ee37ae..0000000 --- a/packages/ai-provider/jest.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import type { Config } from 'jest'; - -const config: Config = { - displayName: 'ai-provider', - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/src'], - testMatch: ['**/*.test.ts'], - - // Skip integration tests by default (they're slow) - testPathIgnorePatterns: ['/node_modules/', '.integration.test.ts$'], - - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/**/*.test.ts', - '!src/**/*.integration.test.ts', - '!src/**/*.d.ts', - ], - - moduleNameMapper: { - '^@cv/(.*)$': '/../$1/src', - }, -}; - -export default config; diff --git a/packages/ai-provider/package.json b/packages/ai-provider/package.json index 4854bb0..d4bb466 100644 --- a/packages/ai-provider/package.json +++ b/packages/ai-provider/package.json @@ -6,10 +6,10 @@ "types": "./src/index.ts", "scripts": { "typecheck": "tsc --noEmit", - "test": "jest", - "test:watch": "jest --watch", - "test:integration": "jest --testPathPattern='.integration.test.ts$' --runInBand", - "test:all": "jest --testPathIgnorePatterns=[]" + "test": "vitest run", + "test:watch": "vitest", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:all": "vitest run --config vitest.integration.config.ts" }, "dependencies": { "@nestjs/common": "^11.1.3", @@ -22,10 +22,8 @@ }, "devDependencies": { "@cv/tsconfig": "*", - "@types/jest": "^29.5.12", "@types/node": "^22.7.5", - "jest": "^29.7.0", - "ts-jest": "^29.1.2", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^4.0.0" } } diff --git a/packages/ai-provider/src/providers/llama-cpp.provider.integration.test.ts b/packages/ai-provider/src/providers/llama-cpp.provider.integration.test.ts index f7e07ad..393ef03 100644 --- a/packages/ai-provider/src/providers/llama-cpp.provider.integration.test.ts +++ b/packages/ai-provider/src/providers/llama-cpp.provider.integration.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeAll } from '@jest/globals'; +import { describe, it, expect, beforeAll } from 'vitest'; import { LlamaCppProvider } from './llama-cpp.provider'; describe('LlamaCppProvider Integration Tests', () => { diff --git a/packages/ai-provider/vitest.config.ts b/packages/ai-provider/vitest.config.ts new file mode 100644 index 0000000..db95ea5 --- /dev/null +++ b/packages/ai-provider/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + root: "./src", + globals: true, + exclude: ["**/*.integration.test.ts"], + passWithNoTests: true, + }, +}); diff --git a/packages/ai-provider/vitest.integration.config.ts b/packages/ai-provider/vitest.integration.config.ts new file mode 100644 index 0000000..89820f2 --- /dev/null +++ b/packages/ai-provider/vitest.integration.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + root: "./src", + globals: true, + include: ["**/*.integration.test.ts"], + }, +}); diff --git a/packages/auth/README.md b/packages/auth/README.md deleted file mode 100644 index c4aff8b..0000000 --- a/packages/auth/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# @cv/auth - -Authentication and authorization package for the CV Generator monorepo. - -## Overview - -This package provides reusable authentication and authorization infrastructure that can be used across multiple applications in the monorepo. It includes user management, token handling, guards, policies, and identity provider system. - -## Package Structure - -``` -src/ -├── auth.module.ts # Main module (global) -├── user/ # User domain entities and services -│ ├── user.entity.ts -│ ├── user.service.ts -│ ├── user.mapper.ts -│ ├── credentials.entity.ts -│ ├── credentials.service.ts -│ └── credentials.mapper.ts -├── token/ # Token management -│ ├── token.service.ts -│ ├── refresh-token.service.ts -│ ├── auth-cookie.service.ts -│ └── token-expiry.config.ts -├── guards/ # Authentication guards -│ ├── jwt-auth.guard.ts -│ └── verified-scope.guard.ts -├── authorization/ # Authorization system -│ ├── authorization.service.ts -│ ├── policy-registry.service.ts -│ ├── policy.interface.ts -│ └── *-resource.policy.ts -├── providers/ # Identity providers -│ └── password/ -│ ├── password-identity-provider.ts -│ ├── password-authentication.service.ts -│ └── credentials/ # Email templates and listeners -├── config/ # Configuration services -├── cookie/ # Cookie utilities -├── metadata/ # Device and location services -├── request/ # Request decorators -├── jwt/ # JWT utilities -└── errors/ # Authentication/authorization errors -``` - -## Usage - -### Installation - -This package is part of the monorepo and is automatically available to other packages via npm workspaces. - -### Importing - -```typescript -import { AuthModule, JwtAuthGuard, UserService } from "@cv/auth"; -``` - -### Global Module - -`AuthModule` is marked as `@Global()`, which means once imported in the root module, all its exports are available throughout the application without needing to import it in every module. - -### Key Exports - -- **Modules**: `AuthModule`, `UserModule`, `TokenModule`, `AuthorizationModule`, `PasswordProviderModule` -- **Services**: `UserService`, `TokenService`, `AuthorizationService`, `PasswordAuthenticationService` -- **Guards**: `JwtAuthGuard`, `VerifiedScopeGuard` -- **Entities**: `User`, `Credentials`, `RefreshToken` -- **Utilities**: `RequestMetadata`, `CookieService`, `AuthCookieService` - -## Dependencies - -- **Peer Dependencies**: `@cv/system`, `@nestjs/common`, `@nestjs/core`, `@nestjs/jwt`, `@nestjs/config` -- **Internal Dependencies**: Only `@cv/system` (infrastructure layer) - -## Architecture - -This package follows the three-layer architecture: -- **Domain Layer**: Entities (`User`, `Credentials`, `RefreshToken`) -- **Service Layer**: Business logic services -- **Guard/Policy Layer**: NestJS guards and authorization policies - -All entities are domain entities that contain business logic and rules, not Prisma models. - diff --git a/packages/auth/package.json b/packages/auth/package.json index ad1aaaa..61faa66 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -4,52 +4,26 @@ "private": true, "main": "./src/index.ts", "types": "./src/index.ts", - "exports": { - ".": { - "require": "./src/index.ts", - "import": "./src/index.ts", - "types": "./src/index.ts" - } - }, - "files": [ - "src/" - ], "scripts": { - "lint": "biome check .", - "lint:fix": "biome check --write .", - "typecheck": "tsc -b" + "typecheck": "tsc --noEmit" }, "dependencies": { - "@cv/system": "*", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", - "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@nestjs/jwt": "^10.2.0", - "bcryptjs": "^2.4.3", - "graphql": "^16.12.0", - "reflect-metadata": "^0.2.2", - "zod": "^4.3.6" + "@cv/core": "workspace:*", + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18", + "@nestjs/graphql": "^13.2.5", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "express": "^5.1.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2" }, "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@cv/biome-config": "*", "@cv/tsconfig": "*", - "@types/bcryptjs": "^2.4.0", "@types/express": "^5.0.0", - "@types/express-serve-static-core": "^5.0.0", - "typescript": "^5.6.3" - }, - "peerDependencies": { - "@cv/system": "*", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", - "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@nestjs/jwt": "^10.2.0", - "bcryptjs": "^2.4.3", - "graphql": "^16.12.0" + "@types/passport-jwt": "^4.0.1", + "typescript": "^5.3.3" } } diff --git a/packages/auth/src/auth.module.ts b/packages/auth/src/auth.module.ts index 11261c7..46d43a4 100644 --- a/packages/auth/src/auth.module.ts +++ b/packages/auth/src/auth.module.ts @@ -1,11 +1,13 @@ import { Global, Module } from "@nestjs/common"; import { DiscoveryModule } from "@nestjs/core"; -import { AuthorizationModule } from "./authorization/authorization.module"; +import { + AuthorizationModule, + IdentityProviderRegistry, + TokenModule, + UserModule, +} from "@cv/core"; import { JwtAuthGuard } from "./guards/jwt-auth.guard"; import { VerifiedScopeGuard } from "./guards/verified-scope.guard"; -import { IdentityProviderRegistry } from "./identity-provider-registry.service"; -import { TokenModule } from "./token/token.module"; -import { UserModule } from "./user/user.module"; @Global() @Module({ diff --git a/packages/auth/src/guards/admin.guard.ts b/packages/auth/src/guards/admin.guard.ts index 498674a..5fd4dd4 100644 --- a/packages/auth/src/guards/admin.guard.ts +++ b/packages/auth/src/guards/admin.guard.ts @@ -5,7 +5,7 @@ import { Injectable, } from "@nestjs/common"; import { GqlExecutionContext } from "@nestjs/graphql"; -import type { User } from "../user/user.entity"; +import type { User } from "@cv/core"; /** * Guard that restricts access to admin users. diff --git a/packages/auth/src/guards/jwt-auth.guard.ts b/packages/auth/src/guards/jwt-auth.guard.ts index 886f94b..f9105c5 100644 --- a/packages/auth/src/guards/jwt-auth.guard.ts +++ b/packages/auth/src/guards/jwt-auth.guard.ts @@ -5,12 +5,12 @@ import { } from "@nestjs/common"; import { GqlExecutionContext } from "@nestjs/graphql"; import { JwtService } from "@nestjs/jwt"; -import { EntityNotFoundError } from "../errors"; import { + EntityNotFoundError, InvalidTokenError, NoTokenError, -} from "../errors/authentication.error"; -import { UserService } from "../user/user.service"; + UserService, +} from "@cv/core"; @Injectable() export class JwtAuthGuard implements CanActivate { diff --git a/packages/auth/src/guards/verified-scope.guard.ts b/packages/auth/src/guards/verified-scope.guard.ts index 187b7e3..e8473d4 100644 --- a/packages/auth/src/guards/verified-scope.guard.ts +++ b/packages/auth/src/guards/verified-scope.guard.ts @@ -6,7 +6,7 @@ import { } from "@nestjs/common"; import { GqlExecutionContext } from "@nestjs/graphql"; import { JwtService } from "@nestjs/jwt"; -import { JwtScope } from "../jwt/jwt-scope.enum"; +import { JwtScope } from "@cv/core"; @Injectable() export class VerifiedScopeGuard implements CanActivate { diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 86b1ffd..c9fb708 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -1,19 +1,2 @@ export * from "./auth.module"; -export * from "./authorization"; -export * from "./config"; -export * from "./cookie"; -export * from "./errors"; export * from "./guards"; -export { - IDENTITY_PROVIDER_KEY, - IdentityProvider as IdentityProviderDecorator, - type IdentityProviderMeta, -} from "./identity-provider.decorator"; -export type { IdentityProvider } from "./identity-provider.interface"; -export * from "./identity-provider-registry.service"; -export * from "./jwt"; -export * from "./metadata"; -export * from "./providers"; -export * from "./request"; -export * from "./token"; -export * from "./user"; diff --git a/packages/auth/src/providers/password/credentials/events/index.ts b/packages/auth/src/providers/password/credentials/events/index.ts deleted file mode 100644 index 93331b9..0000000 --- a/packages/auth/src/providers/password/credentials/events/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./password-reset-requested.event"; -export * from "./registration-attempt-on-existing-email.event"; -export * from "./verification-email-requested.event"; diff --git a/packages/auth/src/providers/password/credentials/events/password-reset-requested.event.ts b/packages/auth/src/providers/password/credentials/events/password-reset-requested.event.ts deleted file mode 100644 index 6c86556..0000000 --- a/packages/auth/src/providers/password/credentials/events/password-reset-requested.event.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Event } from "@cv/system"; - -export const PASSWORD_RESET_REQUESTED = Symbol("password-reset-requested"); - -export class PasswordResetRequestedEvent implements Event { - readonly eventName = PASSWORD_RESET_REQUESTED; - - constructor( - public readonly email: string, - public readonly resetToken: string, - ) {} -} diff --git a/packages/auth/src/providers/password/credentials/events/registration-attempt-on-existing-email.event.ts b/packages/auth/src/providers/password/credentials/events/registration-attempt-on-existing-email.event.ts deleted file mode 100644 index cf2b8e4..0000000 --- a/packages/auth/src/providers/password/credentials/events/registration-attempt-on-existing-email.event.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Event } from "@cv/system"; - -export const REGISTRATION_ATTEMPT_ON_EXISTING_EMAIL = Symbol( - "registration-attempt-on-existing-email", -); - -export class RegistrationAttemptOnExistingEmailEvent implements Event { - readonly eventName = REGISTRATION_ATTEMPT_ON_EXISTING_EMAIL; - - constructor(public readonly email: string) {} -} diff --git a/packages/auth/src/providers/password/credentials/events/verification-email-requested.event.ts b/packages/auth/src/providers/password/credentials/events/verification-email-requested.event.ts deleted file mode 100644 index 049107e..0000000 --- a/packages/auth/src/providers/password/credentials/events/verification-email-requested.event.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Event } from "@cv/system"; - -export const VERIFICATION_EMAIL_REQUESTED = Symbol( - "verification-email-requested", -); - -export class VerificationEmailRequestedEvent implements Event { - readonly eventName = VERIFICATION_EMAIL_REQUESTED; - - constructor( - public readonly userId: string, - public readonly email: string, - public readonly verificationToken: string, - ) {} -} diff --git a/packages/auth/tsconfig.json b/packages/auth/tsconfig.json index 56296f7..93e6448 100644 --- a/packages/auth/tsconfig.json +++ b/packages/auth/tsconfig.json @@ -3,13 +3,11 @@ "compilerOptions": { "outDir": "./dist", "rootDir": "./src", - "baseUrl": ".", - "composite": true, - "paths": { - "@/*": ["./src/*"] - }, - "skipLibCheck": true + "resolveJsonModule": true, + "preserveSymlinks": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true }, "include": ["src/**/*"], - "references": [{ "path": "../system" }, { "path": "../utils" }] + "exclude": ["src/**/*.spec.ts", "src/**/__tests__/**"] } diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..2b6a7c0 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,70 @@ +{ + "name": "@cv/core", + "version": "0.0.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit", + "build": "tsup" + }, + "dependencies": { + "@cv/ai-parser": "workspace:*", + "@cv/ai-provider": "workspace:*", + "@cv/cv-renderer": "workspace:*", + "@cv/file-storage": "workspace:*", + "@cv/file-upload": "workspace:*", + "@cv/utils": "workspace:*", + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18", + "@nestjs/event-emitter": "^3.0.1", + "@nestjs/graphql": "^13.2.5", + "@nestjs/jwt": "^11.0.2", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.1.18", + "@prisma/adapter-pg": "^7.1.0", + "@prisma/client": "^7.1.0", + "@riotbyte-com/nest-service-locator": "0.3.0-rc.4", + "@riotbyte-com/project-q-core": "1.0.19-rc.3", + "@riotbyte-com/project-q-nestjs": "1.0.19-rc.3", + "@riotbyte-com/project-q-prisma": "1.0.19-rc.3", + "bcryptjs": "^2.4.3", + "canvas": "^3.1.0", + "dataloader": "^2.2.3", + "express": "^5.1.0", + "graphql": "^16.12.0", + "handlebars": "^4.7.8", + "mammoth": "^1.11.0", + "multer": "^2.0.2", + "nest-commander": "^3.16.0", + "nestjs-zod": "^5.1.1", + "neverthrow": "^8.2.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "passport-local": "^1.0.0", + "pdf-parse": "^1.1.4", + "pg": "^8.16.3", + "reflect-metadata": "^0.2.2", + "resend": "^6.5.2", + "rxjs": "^7.8.1", + "turndown": "^7.2.0", + "zod": "^4.3.6" + }, + "devDependencies": { + "@cv/tsconfig": "*", + "@faker-js/faker": "^9.9.0", + "@types/bcryptjs": "^2.4.6", + "@types/express": "^5.0.0", + "@types/express-serve-static-core": "^5.0.0", + "@types/multer": "^2.0.0", + "@types/node": "^22.7.5", + "@types/passport-jwt": "^4.0.1", + "@types/passport-local": "^1.0.38", + "@types/pdf-parse": "^1.1.5", + "@types/pg": "^8.15.6", + "@types/turndown": "^5.0.5", + "tsup": "^8.5.1", + "typescript": "^5.3.3" + } +} diff --git a/apps/server/prisma.config.ts b/packages/core/prisma.config.ts similarity index 100% rename from apps/server/prisma.config.ts rename to packages/core/prisma.config.ts diff --git a/apps/server/prisma/migrations/20251017094729_init/migration.sql b/packages/core/prisma/migrations/20251017094729_init/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251017094729_init/migration.sql rename to packages/core/prisma/migrations/20251017094729_init/migration.sql diff --git a/apps/server/prisma/migrations/20251017115341_add_job_entities/migration.sql b/packages/core/prisma/migrations/20251017115341_add_job_entities/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251017115341_add_job_entities/migration.sql rename to packages/core/prisma/migrations/20251017115341_add_job_entities/migration.sql diff --git a/apps/server/prisma/migrations/20251018092856_add_organizations/migration.sql b/packages/core/prisma/migrations/20251018092856_add_organizations/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251018092856_add_organizations/migration.sql rename to packages/core/prisma/migrations/20251018092856_add_organizations/migration.sql diff --git a/apps/server/prisma/migrations/20251019210751_add_organization_roles/migration.sql b/packages/core/prisma/migrations/20251019210751_add_organization_roles/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251019210751_add_organization_roles/migration.sql rename to packages/core/prisma/migrations/20251019210751_add_organization_roles/migration.sql diff --git a/apps/server/prisma/migrations/20251019212422_add_role_colors/migration.sql b/packages/core/prisma/migrations/20251019212422_add_role_colors/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251019212422_add_role_colors/migration.sql rename to packages/core/prisma/migrations/20251019212422_add_role_colors/migration.sql diff --git a/apps/server/prisma/migrations/20251019214831_add_vacancies/migration.sql b/packages/core/prisma/migrations/20251019214831_add_vacancies/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251019214831_add_vacancies/migration.sql rename to packages/core/prisma/migrations/20251019214831_add_vacancies/migration.sql diff --git a/apps/server/prisma/migrations/20251025120015_add_cv_template_system/migration.sql b/packages/core/prisma/migrations/20251025120015_add_cv_template_system/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251025120015_add_cv_template_system/migration.sql rename to packages/core/prisma/migrations/20251025120015_add_cv_template_system/migration.sql diff --git a/apps/server/prisma/migrations/20251026154204_simplify_cv_template_schema/migration.sql b/packages/core/prisma/migrations/20251026154204_simplify_cv_template_schema/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026154204_simplify_cv_template_schema/migration.sql rename to packages/core/prisma/migrations/20251026154204_simplify_cv_template_schema/migration.sql diff --git a/apps/server/prisma/migrations/20251026160424_rename_user_organizations_to_memberships/migration.sql b/packages/core/prisma/migrations/20251026160424_rename_user_organizations_to_memberships/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026160424_rename_user_organizations_to_memberships/migration.sql rename to packages/core/prisma/migrations/20251026160424_rename_user_organizations_to_memberships/migration.sql diff --git a/apps/server/prisma/migrations/20251026203242_add_cv_fields/migration.sql b/packages/core/prisma/migrations/20251026203242_add_cv_fields/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026203242_add_cv_fields/migration.sql rename to packages/core/prisma/migrations/20251026203242_add_cv_fields/migration.sql diff --git a/apps/server/prisma/migrations/20251026203330_refactor_cv_and_vacancy_entities/migration.sql b/packages/core/prisma/migrations/20251026203330_refactor_cv_and_vacancy_entities/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026203330_refactor_cv_and_vacancy_entities/migration.sql rename to packages/core/prisma/migrations/20251026203330_refactor_cv_and_vacancy_entities/migration.sql diff --git a/apps/server/prisma/migrations/20251026203431_add_job_type_entity/migration.sql b/packages/core/prisma/migrations/20251026203431_add_job_type_entity/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026203431_add_job_type_entity/migration.sql rename to packages/core/prisma/migrations/20251026203431_add_job_type_entity/migration.sql diff --git a/apps/server/prisma/migrations/20251026203550_add_vacancy_owner_and_public/migration.sql b/packages/core/prisma/migrations/20251026203550_add_vacancy_owner_and_public/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026203550_add_vacancy_owner_and_public/migration.sql rename to packages/core/prisma/migrations/20251026203550_add_vacancy_owner_and_public/migration.sql diff --git a/apps/server/prisma/migrations/20251026204100_add_application_and_education_entities/migration.sql b/packages/core/prisma/migrations/20251026204100_add_application_and_education_entities/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026204100_add_application_and_education_entities/migration.sql rename to packages/core/prisma/migrations/20251026204100_add_application_and_education_entities/migration.sql diff --git a/apps/server/prisma/migrations/20251026204243_rename_education_to_institution/migration.sql b/packages/core/prisma/migrations/20251026204243_rename_education_to_institution/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026204243_rename_education_to_institution/migration.sql rename to packages/core/prisma/migrations/20251026204243_rename_education_to_institution/migration.sql diff --git a/apps/server/prisma/migrations/20251026204357_remove_userid_from_vacancy/migration.sql b/packages/core/prisma/migrations/20251026204357_remove_userid_from_vacancy/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026204357_remove_userid_from_vacancy/migration.sql rename to packages/core/prisma/migrations/20251026204357_remove_userid_from_vacancy/migration.sql diff --git a/apps/server/prisma/migrations/20251026204432_add_application_status_enum/migration.sql b/packages/core/prisma/migrations/20251026204432_add_application_status_enum/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026204432_add_application_status_enum/migration.sql rename to packages/core/prisma/migrations/20251026204432_add_application_status_enum/migration.sql diff --git a/apps/server/prisma/migrations/20251026204502_remove_education_from_cv/migration.sql b/packages/core/prisma/migrations/20251026204502_remove_education_from_cv/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026204502_remove_education_from_cv/migration.sql rename to packages/core/prisma/migrations/20251026204502_remove_education_from_cv/migration.sql diff --git a/apps/server/prisma/migrations/20251026204631_add_application_status_entity/migration.sql b/packages/core/prisma/migrations/20251026204631_add_application_status_entity/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251026204631_add_application_status_entity/migration.sql rename to packages/core/prisma/migrations/20251026204631_add_application_status_entity/migration.sql diff --git a/apps/server/prisma/migrations/20251029130211_add_min_max_salary/migration.sql b/packages/core/prisma/migrations/20251029130211_add_min_max_salary/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251029130211_add_min_max_salary/migration.sql rename to packages/core/prisma/migrations/20251029130211_add_min_max_salary/migration.sql diff --git a/apps/server/prisma/migrations/20251029135936_migrate_salary_to_min_max/migration.sql b/packages/core/prisma/migrations/20251029135936_migrate_salary_to_min_max/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251029135936_migrate_salary_to_min_max/migration.sql rename to packages/core/prisma/migrations/20251029135936_migrate_salary_to_min_max/migration.sql diff --git a/apps/server/prisma/migrations/20251103203729_add_skills_to_education/migration.sql b/packages/core/prisma/migrations/20251103203729_add_skills_to_education/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251103203729_add_skills_to_education/migration.sql rename to packages/core/prisma/migrations/20251103203729_add_skills_to_education/migration.sql diff --git a/apps/server/prisma/migrations/20251207163643_add_credentials_table/migration.sql b/packages/core/prisma/migrations/20251207163643_add_credentials_table/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251207163643_add_credentials_table/migration.sql rename to packages/core/prisma/migrations/20251207163643_add_credentials_table/migration.sql diff --git a/apps/server/prisma/migrations/20251207201916_add_email_verification_and_password_reset_fields/migration.sql b/packages/core/prisma/migrations/20251207201916_add_email_verification_and_password_reset_fields/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251207201916_add_email_verification_and_password_reset_fields/migration.sql rename to packages/core/prisma/migrations/20251207201916_add_email_verification_and_password_reset_fields/migration.sql diff --git a/apps/server/prisma/migrations/20251207201931_add_unique_constraints_to_tokens/migration.sql b/packages/core/prisma/migrations/20251207201931_add_unique_constraints_to_tokens/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251207201931_add_unique_constraints_to_tokens/migration.sql rename to packages/core/prisma/migrations/20251207201931_add_unique_constraints_to_tokens/migration.sql diff --git a/apps/server/prisma/migrations/20251207215106_remove_email_verified_boolean/migration.sql b/packages/core/prisma/migrations/20251207215106_remove_email_verified_boolean/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251207215106_remove_email_verified_boolean/migration.sql rename to packages/core/prisma/migrations/20251207215106_remove_email_verified_boolean/migration.sql diff --git a/apps/server/prisma/migrations/20251209174757_add_refresh_tokens/migration.sql b/packages/core/prisma/migrations/20251209174757_add_refresh_tokens/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251209174757_add_refresh_tokens/migration.sql rename to packages/core/prisma/migrations/20251209174757_add_refresh_tokens/migration.sql diff --git a/apps/server/prisma/migrations/20251209175507_add_encrypted_refresh_token/migration.sql b/packages/core/prisma/migrations/20251209175507_add_encrypted_refresh_token/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251209175507_add_encrypted_refresh_token/migration.sql rename to packages/core/prisma/migrations/20251209175507_add_encrypted_refresh_token/migration.sql diff --git a/apps/server/prisma/migrations/20251209180341_add_device_location_to_refresh_tokens/migration.sql b/packages/core/prisma/migrations/20251209180341_add_device_location_to_refresh_tokens/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20251209180341_add_device_location_to_refresh_tokens/migration.sql rename to packages/core/prisma/migrations/20251209180341_add_device_location_to_refresh_tokens/migration.sql diff --git a/apps/server/prisma/migrations/20260207151745_add_user_ai_settings/migration.sql b/packages/core/prisma/migrations/20260207151745_add_user_ai_settings/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260207151745_add_user_ai_settings/migration.sql rename to packages/core/prisma/migrations/20260207151745_add_user_ai_settings/migration.sql diff --git a/apps/server/prisma/migrations/20260207221010_add_user_profile/migration.sql b/packages/core/prisma/migrations/20260207221010_add_user_profile/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260207221010_add_user_profile/migration.sql rename to packages/core/prisma/migrations/20260207221010_add_user_profile/migration.sql diff --git a/apps/server/prisma/migrations/20260209211505_add_data_import_models/migration.sql b/packages/core/prisma/migrations/20260209211505_add_data_import_models/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260209211505_add_data_import_models/migration.sql rename to packages/core/prisma/migrations/20260209211505_add_data_import_models/migration.sql diff --git a/apps/server/prisma/migrations/20260210004726_add_cv_template_content_fields/migration.sql b/packages/core/prisma/migrations/20260210004726_add_cv_template_content_fields/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260210004726_add_cv_template_content_fields/migration.sql rename to packages/core/prisma/migrations/20260210004726_add_cv_template_content_fields/migration.sql diff --git a/apps/server/prisma/migrations/20260210120000_add_ai_call_log/migration.sql b/packages/core/prisma/migrations/20260210120000_add_ai_call_log/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260210120000_add_ai_call_log/migration.sql rename to packages/core/prisma/migrations/20260210120000_add_ai_call_log/migration.sql diff --git a/apps/server/prisma/migrations/20260210214526_add_user_profile_full_name/migration.sql b/packages/core/prisma/migrations/20260210214526_add_user_profile_full_name/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260210214526_add_user_profile_full_name/migration.sql rename to packages/core/prisma/migrations/20260210214526_add_user_profile_full_name/migration.sql diff --git a/apps/server/prisma/migrations/20260216120000_multi_profile/migration.sql b/packages/core/prisma/migrations/20260216120000_multi_profile/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260216120000_multi_profile/migration.sql rename to packages/core/prisma/migrations/20260216120000_multi_profile/migration.sql diff --git a/apps/server/prisma/migrations/20260217120000_add_file_hash/migration.sql b/packages/core/prisma/migrations/20260217120000_add_file_hash/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260217120000_add_file_hash/migration.sql rename to packages/core/prisma/migrations/20260217120000_add_file_hash/migration.sql diff --git a/apps/server/prisma/migrations/20260218120000_add_user_role/migration.sql b/packages/core/prisma/migrations/20260218120000_add_user_role/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260218120000_add_user_role/migration.sql rename to packages/core/prisma/migrations/20260218120000_add_user_role/migration.sql diff --git a/apps/server/prisma/migrations/20260222120000_add_queue_schema/migration.sql b/packages/core/prisma/migrations/20260222120000_add_queue_schema/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260222120000_add_queue_schema/migration.sql rename to packages/core/prisma/migrations/20260222120000_add_queue_schema/migration.sql diff --git a/apps/server/prisma/migrations/20260222130000_add_worker_heartbeats/migration.sql b/packages/core/prisma/migrations/20260222130000_add_worker_heartbeats/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260222130000_add_worker_heartbeats/migration.sql rename to packages/core/prisma/migrations/20260222130000_add_worker_heartbeats/migration.sql diff --git a/apps/server/prisma/migrations/20260225193547_replace_queue_with_prisma_transport/migration.sql b/packages/core/prisma/migrations/20260225193547_replace_queue_with_prisma_transport/migration.sql similarity index 100% rename from apps/server/prisma/migrations/20260225193547_replace_queue_with_prisma_transport/migration.sql rename to packages/core/prisma/migrations/20260225193547_replace_queue_with_prisma_transport/migration.sql diff --git a/packages/core/prisma/migrations/20260412120000_add_external_import_fields_to_vacancy/migration.sql b/packages/core/prisma/migrations/20260412120000_add_external_import_fields_to_vacancy/migration.sql new file mode 100644 index 0000000..f83ae78 --- /dev/null +++ b/packages/core/prisma/migrations/20260412120000_add_external_import_fields_to_vacancy/migration.sql @@ -0,0 +1,14 @@ +-- AlterTable +ALTER TABLE "vacancies" ADD COLUMN "externalId" TEXT, +ADD COLUMN "source" TEXT, +ADD COLUMN "externalApplyUrl" TEXT, +ADD COLUMN "descriptionHtml" TEXT, +ADD COLUMN "hoursFrom" INTEGER, +ADD COLUMN "hoursTo" INTEGER, +ADD COLUMN "province" TEXT, +ADD COLUMN "rateType" TEXT, +ADD COLUMN "contractType" TEXT, +ADD COLUMN "publishedAt" TIMESTAMP(3); + +-- CreateIndex +CREATE UNIQUE INDEX "vacancies_source_externalId_key" ON "vacancies"("source", "externalId"); diff --git a/packages/core/prisma/migrations/20260412130000_extract_vacancy_string_fields_to_entities/migration.sql b/packages/core/prisma/migrations/20260412130000_extract_vacancy_string_fields_to_entities/migration.sql new file mode 100644 index 0000000..c6e0028 --- /dev/null +++ b/packages/core/prisma/migrations/20260412130000_extract_vacancy_string_fields_to_entities/migration.sql @@ -0,0 +1,77 @@ +-- CreateEnum +CREATE TYPE "VacancySource" AS ENUM ('MANUAL'); + +-- CreateEnum +CREATE TYPE "LocationType" AS ENUM ('COUNTRY', 'PROVINCE', 'CITY', 'DISTRICT'); + +-- CreateTable +CREATE TABLE "locations" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "type" "LocationType" NOT NULL, + "code" TEXT, + "parentId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "locations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "rate_types" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "rate_types_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "contract_types" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "contract_types_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "locations_type_code_key" ON "locations"("type", "code"); +CREATE UNIQUE INDEX "rate_types_name_key" ON "rate_types"("name"); +CREATE UNIQUE INDEX "contract_types_name_key" ON "contract_types"("name"); + +-- AddForeignKey (location self-referential) +ALTER TABLE "locations" ADD CONSTRAINT "locations_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "locations"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AlterTable: add new FK columns, convert source to enum, drop old string columns +ALTER TABLE "vacancies" + ADD COLUMN "rateTypeId" TEXT, + ADD COLUMN "contractTypeId" TEXT, + ADD COLUMN "locationId" TEXT; + +-- Convert source from String to VacancySource enum +-- Drop the unique constraint that references the old source column +DROP INDEX IF EXISTS "vacancies_source_externalId_key"; + +-- Alter the column type +ALTER TABLE "vacancies" + ALTER COLUMN "source" TYPE "VacancySource" USING "source"::"VacancySource"; + +-- Re-add the unique constraint +CREATE UNIQUE INDEX "vacancies_source_externalId_key" ON "vacancies"("source", "externalId"); + +-- Drop old string columns (data was denormalized) +ALTER TABLE "vacancies" + DROP COLUMN IF EXISTS "location", + DROP COLUMN IF EXISTS "province", + DROP COLUMN IF EXISTS "rateType", + DROP COLUMN IF EXISTS "contractType"; + +-- AddForeignKey +ALTER TABLE "vacancies" ADD CONSTRAINT "vacancies_rateTypeId_fkey" FOREIGN KEY ("rateTypeId") REFERENCES "rate_types"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "vacancies" ADD CONSTRAINT "vacancies_contractTypeId_fkey" FOREIGN KEY ("contractTypeId") REFERENCES "contract_types"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "vacancies" ADD CONSTRAINT "vacancies_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "locations"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/core/prisma/migrations/20260412140000_drop_description_html_from_vacancy/migration.sql b/packages/core/prisma/migrations/20260412140000_drop_description_html_from_vacancy/migration.sql new file mode 100644 index 0000000..98e6eb5 --- /dev/null +++ b/packages/core/prisma/migrations/20260412140000_drop_description_html_from_vacancy/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable: drop descriptionHtml — description is now always markdown +ALTER TABLE "vacancies" DROP COLUMN IF EXISTS "descriptionHtml"; diff --git a/packages/core/prisma/migrations/20260412150000_profile_location_fk/migration.sql b/packages/core/prisma/migrations/20260412150000_profile_location_fk/migration.sql new file mode 100644 index 0000000..24b3331 --- /dev/null +++ b/packages/core/prisma/migrations/20260412150000_profile_location_fk/migration.sql @@ -0,0 +1,11 @@ +-- AlterTable +ALTER TABLE "profiles" ADD COLUMN "locationId" TEXT; + +-- DropColumn +ALTER TABLE "profiles" DROP COLUMN "city"; + +-- DropColumn +ALTER TABLE "profiles" DROP COLUMN "country"; + +-- AddForeignKey +ALTER TABLE "profiles" ADD CONSTRAINT "profiles_locationId_fkey" FOREIGN KEY ("locationId") REFERENCES "locations"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/server/prisma/migrations/migration_lock.toml b/packages/core/prisma/migrations/migration_lock.toml similarity index 100% rename from apps/server/prisma/migrations/migration_lock.toml rename to packages/core/prisma/migrations/migration_lock.toml diff --git a/apps/server/prisma/models/ai-call-log.prisma b/packages/core/prisma/models/ai-call-log.prisma similarity index 100% rename from apps/server/prisma/models/ai-call-log.prisma rename to packages/core/prisma/models/ai-call-log.prisma diff --git a/apps/server/prisma/models/cv.prisma b/packages/core/prisma/models/cv.prisma similarity index 100% rename from apps/server/prisma/models/cv.prisma rename to packages/core/prisma/models/cv.prisma diff --git a/apps/server/prisma/models/data-import.prisma b/packages/core/prisma/models/data-import.prisma similarity index 100% rename from apps/server/prisma/models/data-import.prisma rename to packages/core/prisma/models/data-import.prisma diff --git a/apps/server/prisma/models/job-experience.prisma b/packages/core/prisma/models/job-experience.prisma similarity index 100% rename from apps/server/prisma/models/job-experience.prisma rename to packages/core/prisma/models/job-experience.prisma diff --git a/apps/server/prisma/models/organization.prisma b/packages/core/prisma/models/organization.prisma similarity index 100% rename from apps/server/prisma/models/organization.prisma rename to packages/core/prisma/models/organization.prisma diff --git a/apps/server/prisma/models/profile.prisma b/packages/core/prisma/models/profile.prisma similarity index 84% rename from apps/server/prisma/models/profile.prisma rename to packages/core/prisma/models/profile.prisma index 512ca95..cdb4cf0 100644 --- a/apps/server/prisma/models/profile.prisma +++ b/packages/core/prisma/models/profile.prisma @@ -7,8 +7,7 @@ model Profile { encryptedPhone String? encryptedAddress String? encryptedPostalCode String? - city String? - country String? + locationId String? website String? linkedInUrl String? summary String? @@ -16,6 +15,7 @@ model Profile { updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade) + location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull) educations Education[] jobExperiences UserJobExperience[] cvs CV[] diff --git a/apps/server/prisma/models/queue.prisma b/packages/core/prisma/models/queue.prisma similarity index 100% rename from apps/server/prisma/models/queue.prisma rename to packages/core/prisma/models/queue.prisma diff --git a/apps/server/prisma/models/refresh-token.prisma b/packages/core/prisma/models/refresh-token.prisma similarity index 100% rename from apps/server/prisma/models/refresh-token.prisma rename to packages/core/prisma/models/refresh-token.prisma diff --git a/apps/server/prisma/models/user-ai-settings.prisma b/packages/core/prisma/models/user-ai-settings.prisma similarity index 100% rename from apps/server/prisma/models/user-ai-settings.prisma rename to packages/core/prisma/models/user-ai-settings.prisma diff --git a/apps/server/prisma/models/user.prisma b/packages/core/prisma/models/user.prisma similarity index 100% rename from apps/server/prisma/models/user.prisma rename to packages/core/prisma/models/user.prisma diff --git a/packages/core/prisma/models/vacancy.prisma b/packages/core/prisma/models/vacancy.prisma new file mode 100644 index 0000000..2893966 --- /dev/null +++ b/packages/core/prisma/models/vacancy.prisma @@ -0,0 +1,145 @@ +enum VacancySource { + MANUAL +} + +enum LocationType { + COUNTRY + PROVINCE + CITY + DISTRICT +} + +model Location { + id String @id @default(cuid()) + name String + type LocationType + code String? + parentId String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + parent Location? @relation("LocationHierarchy", fields: [parentId], references: [id]) + children Location[] @relation("LocationHierarchy") + vacancies Vacancy[] + profiles Profile[] + + @@unique([type, code]) + @@map("locations") +} + +model RateType { + id String @id @default(cuid()) + name String @unique + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + vacancies Vacancy[] + + @@map("rate_types") +} + +model ContractType { + id String @id @default(cuid()) + name String @unique + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + vacancies Vacancy[] + + @@map("contract_types") +} + +model JobType { + id String @id @default(cuid()) + name String @unique + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + vacancies Vacancy[] + + @@map("job_types") +} + +model ApplicationStatus { + id String @id @default(cuid()) + name String @unique + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + applications Application[] + + @@map("application_statuses") +} + +model Vacancy { + id String @id @default(cuid()) + ownerId String + title String + companyId String + roleId String + levelId String? + jobTypeId String? + rateTypeId String? + contractTypeId String? + locationId String? + description String? + requirements String? + minSalary Int? + maxSalary Int? + applicationUrl String? + deadline DateTime? + hoursFrom Int? + hoursTo Int? + isActive Boolean @default(true) + isPublic Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // External import fields + externalId String? + source VacancySource? + externalApplyUrl String? + publishedAt DateTime? + + // Relations + owner User @relation("VacancyOwner", fields: [ownerId], references: [id], onDelete: Cascade) + company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + level Level? @relation(fields: [levelId], references: [id], onDelete: Cascade) + jobType JobType? @relation(fields: [jobTypeId], references: [id], onDelete: Cascade) + rateType RateType? @relation(fields: [rateTypeId], references: [id], onDelete: SetNull) + contractType ContractType? @relation(fields: [contractTypeId], references: [id], onDelete: SetNull) + location Location? @relation(fields: [locationId], references: [id], onDelete: SetNull) + skills Skill[] + applications Application[] + + @@unique([source, externalId]) + @@map("vacancies") +} + +model Application { + id String @id @default(cuid()) + userId String + profileId String + vacancyId String + cvId String? + coverLetter String? + statusId String + appliedAt DateTime @default(now()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + profile Profile @relation(fields: [profileId], references: [id], onDelete: Cascade) + vacancy Vacancy @relation(fields: [vacancyId], references: [id], onDelete: Cascade) + cv CV? @relation(fields: [cvId], references: [id], onDelete: SetNull) + status ApplicationStatus @relation(fields: [statusId], references: [id], onDelete: Restrict) + + @@unique([userId, vacancyId]) + @@map("applications") +} diff --git a/apps/server/prisma/schema.prisma b/packages/core/prisma/schema.prisma similarity index 100% rename from apps/server/prisma/schema.prisma rename to packages/core/prisma/schema.prisma diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..8126836 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,5 @@ +export * from "./shared"; +export * from "./modules/database"; +export * from "./modules/mail"; +export * from "./modules/auth"; +export * from "./modules/events"; diff --git a/packages/core/src/modules/admin/admin.module.ts b/packages/core/src/modules/admin/admin.module.ts new file mode 100644 index 0000000..45c6231 --- /dev/null +++ b/packages/core/src/modules/admin/admin.module.ts @@ -0,0 +1,31 @@ +import { AIModule } from "@cv/ai-provider"; +import { BaseModule } from "../../shared"; +import { Module } from "@nestjs/common"; +import { ApplicationStatusModule } from "../application/application-status/application-status.module"; +import { EducationModule } from "../education/education.module"; +import { CompanyModule } from "../job-experience/company/company.module"; +import { LevelModule } from "../job-experience/level/level.module"; +import { RoleModule } from "../job-experience/role/role.module"; +import { SkillModule } from "../job-experience/skill/skill.module"; +import { OrganizationModule } from "../organization/organization.module"; +import { JobTypeModule } from "../vacancies/job-type/job-type.module"; +import { DatabaseModule } from "../database/database.module"; +import { QueueMonitorService } from "./queue-monitor.service"; + +@Module({ + imports: [ + AIModule.forConfig(), + BaseModule, + DatabaseModule, + SkillModule, + CompanyModule, + RoleModule, + LevelModule, + EducationModule, + JobTypeModule, + ApplicationStatusModule, + OrganizationModule, + ], + providers: [QueueMonitorService], +}) +export class AdminModule {} diff --git a/packages/core/src/modules/admin/ai-call-log-persistence.service.ts b/packages/core/src/modules/admin/ai-call-log-persistence.service.ts new file mode 100644 index 0000000..95ca51a --- /dev/null +++ b/packages/core/src/modules/admin/ai-call-log-persistence.service.ts @@ -0,0 +1,54 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { PrismaService } from "../database/prisma.service"; +import type { AiCallLogEntry } from "./ai-call-log.service"; + +interface QueryOptions { + limit?: number; + status?: string; + providerName?: string; +} + +/** + * Persists AI call log entries to the database. + * All writes are fire-and-forget to avoid impacting AI call latency. + */ +@Injectable() +export class AiCallLogPersistenceService { + private readonly logger = new Logger(AiCallLogPersistenceService.name); + + constructor(private readonly prisma: PrismaService) {} + + persist(entry: AiCallLogEntry): void { + this.prisma.aiCallLog + .create({ + data: { + id: entry.id, + userId: entry.userId ?? null, + providerName: entry.providerName, + model: entry.model ?? null, + source: entry.source ?? null, + durationMs: entry.durationMs, + promptTokens: entry.promptTokens ?? null, + completionTokens: entry.completionTokens ?? null, + finishReason: entry.finishReason ?? null, + status: entry.status, + error: entry.error ?? null, + createdAt: new Date(entry.timestamp), + }, + }) + .catch((err) => { + this.logger.error("Failed to persist AI call log entry", err); + }); + } + + async query(options: QueryOptions = {}) { + return this.prisma.aiCallLog.findMany({ + where: { + ...(options.status ? { status: options.status } : {}), + ...(options.providerName ? { providerName: options.providerName } : {}), + }, + orderBy: { createdAt: "desc" }, + take: options.limit ?? 100, + }); + } +} diff --git a/packages/core/src/modules/admin/ai-call-log.module.ts b/packages/core/src/modules/admin/ai-call-log.module.ts new file mode 100644 index 0000000..5e4b370 --- /dev/null +++ b/packages/core/src/modules/admin/ai-call-log.module.ts @@ -0,0 +1,12 @@ +import { Global, Module } from "@nestjs/common"; +import { DatabaseModule } from "../database/database.module"; +import { AiCallLogPersistenceService } from "./ai-call-log-persistence.service"; +import { AiCallLogService } from "./ai-call-log.service"; + +@Global() +@Module({ + imports: [DatabaseModule], + providers: [AiCallLogPersistenceService, AiCallLogService], + exports: [AiCallLogService], +}) +export class AiCallLogModule {} diff --git a/packages/core/src/modules/admin/ai-call-log.service.ts b/packages/core/src/modules/admin/ai-call-log.service.ts new file mode 100644 index 0000000..810214e --- /dev/null +++ b/packages/core/src/modules/admin/ai-call-log.service.ts @@ -0,0 +1,54 @@ +import { Injectable, Optional } from "@nestjs/common"; +import { AiCallLogPersistenceService } from "./ai-call-log-persistence.service"; + +export interface AiCallLogEntry { + id: string; + timestamp: string; + providerName: string; + durationMs: number; + promptTokens?: number | undefined; + completionTokens?: number | undefined; + model?: string | undefined; + finishReason?: string | undefined; + status: "success" | "error"; + error?: string | undefined; + userId?: string | undefined; + source?: string | undefined; +} + +const MAX_ENTRIES = 100; + +/** + * In-memory ring buffer for AI provider call history. + * Retains the last 100 entries; resets on server restart. + * Optionally persists to DB via AiCallLogPersistenceService. + */ +@Injectable() +export class AiCallLogService { + private readonly entries: AiCallLogEntry[] = []; + + constructor( + @Optional() + private readonly persistenceService?: AiCallLogPersistenceService, + ) {} + + record(entry: AiCallLogEntry): void { + this.entries.unshift(entry); + if (this.entries.length > MAX_ENTRIES) { + this.entries.length = MAX_ENTRIES; + } + this.persistenceService?.persist(entry); + } + + getEntries(limit?: number): AiCallLogEntry[] { + return limit ? this.entries.slice(0, limit) : [...this.entries]; + } + + async queryHistory(options?: { + limit?: number; + status?: string; + providerName?: string; + }) { + return this.persistenceService?.query(options) ?? []; + } +} diff --git a/packages/core/src/modules/admin/logging-ai-provider.ts b/packages/core/src/modules/admin/logging-ai-provider.ts new file mode 100644 index 0000000..b204911 --- /dev/null +++ b/packages/core/src/modules/admin/logging-ai-provider.ts @@ -0,0 +1,83 @@ +import { randomUUID } from "node:crypto"; +import type { + AICompletionRequest, + AICompletionResponse, + AIProvider, + AIProviderStatus, +} from "@cv/ai-provider"; +import { AiCallLogService } from "./ai-call-log.service"; + +interface LoggingOptions { + userId?: string; + source?: string; +} + +/** + * Thin wrapper around an AIProvider that records call timing and + * response metadata to the in-memory AiCallLogService. + */ +export class LoggingAIProvider implements AIProvider { + readonly name: string; + + constructor( + private readonly inner: AIProvider, + private readonly logService: AiCallLogService, + private readonly options: LoggingOptions = {}, + ) { + this.name = inner.name; + } + + async complete(request: AICompletionRequest): Promise { + const start = performance.now(); + + try { + const response = await this.inner.complete(request); + const durationMs = Math.round(performance.now() - start); + + this.logService.record({ + id: randomUUID(), + timestamp: new Date().toISOString(), + providerName: this.name, + durationMs, + promptTokens: response.promptTokens, + completionTokens: response.completionTokens, + model: response.model, + finishReason: response.finishReason, + status: "success", + userId: this.options.userId, + source: this.options.source, + }); + + return response; + } catch (err) { + const durationMs = Math.round(performance.now() - start); + + this.logService.record({ + id: randomUUID(), + timestamp: new Date().toISOString(), + providerName: this.name, + durationMs, + status: "error", + error: err instanceof Error ? err.message : String(err), + userId: this.options.userId, + source: this.options.source, + }); + + throw err; + } + } + + isHealthy(): Promise { + return this.inner.isHealthy(); + } + + getStatus(): Promise { + return ( + this.inner.getStatus?.() ?? + Promise.resolve({ + healthy: false, + providerName: this.name, + }) + ); + } +} diff --git a/packages/core/src/modules/admin/queue-monitor.service.ts b/packages/core/src/modules/admin/queue-monitor.service.ts new file mode 100644 index 0000000..39e107d --- /dev/null +++ b/packages/core/src/modules/admin/queue-monitor.service.ts @@ -0,0 +1,124 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "../database/prisma.service"; + +interface QueueStatsRow { + pending: bigint; + scheduled: bigint; + processing: bigint; + oldest_pending_seconds: number | null; +} + +interface QueueMessageRow { + id: string; + queue_name: string; + message_name: string | null; + created_at: Date; + available_at: Date; + delivered_at: Date | null; +} + +interface WorkerHeartbeatRow { + worker_id: string; + started_at: Date; + last_seen_at: Date; +} + +export interface QueueStatsResult { + pending: number; + scheduled: number; + processing: number; + oldestPendingSeconds: number | null; +} + +export interface QueueMessageResult { + id: string; + queueName: string; + messageName: string | null; + status: "pending" | "scheduled" | "processing"; + createdAt: Date; +} + +export interface WorkerHealthResult { + workerId: string; + status: "healthy" | "stale" | "dead"; + startedAt: Date; + lastSeenAt: Date; +} + +const HEALTHY_THRESHOLD_SECONDS = 60; +const STALE_THRESHOLD_SECONDS = 300; + +const deriveWorkerStatus = ( + lastSeenAt: Date, +): "healthy" | "stale" | "dead" => { + const ageSeconds = (Date.now() - lastSeenAt.getTime()) / 1000; + if (ageSeconds <= HEALTHY_THRESHOLD_SECONDS) return "healthy"; + if (ageSeconds <= STALE_THRESHOLD_SECONDS) return "stale"; + return "dead"; +}; + +const deriveMessageStatus = ( + row: QueueMessageRow, +): "pending" | "scheduled" | "processing" => { + if (row.delivered_at) return "processing"; + return row.available_at > new Date() ? "scheduled" : "pending"; +}; + +/** + * Read-only monitoring service for the project-q queue tables. + * Uses raw SQL since queue tables live in the `queue` schema (not managed by Prisma). + */ +@Injectable() +export class QueueMonitorService { + constructor(private readonly prisma: PrismaService) {} + + async getStats(): Promise { + const rows = await this.prisma.$queryRaw` + SELECT + COUNT(*) FILTER (WHERE delivered_at IS NULL AND available_at <= now()) AS pending, + COUNT(*) FILTER (WHERE delivered_at IS NULL AND available_at > now()) AS scheduled, + COUNT(*) FILTER (WHERE delivered_at IS NOT NULL) AS processing, + EXTRACT(EPOCH FROM now() - MIN(available_at) FILTER (WHERE delivered_at IS NULL AND available_at <= now())) AS oldest_pending_seconds + FROM queue.messages + `; + + const row = rows[0]; + return { + pending: Number(row?.pending ?? 0), + scheduled: Number(row?.scheduled ?? 0), + processing: Number(row?.processing ?? 0), + oldestPendingSeconds: row?.oldest_pending_seconds ?? null, + }; + } + + async getMessages(limit = 50): Promise { + const clampedLimit = Math.max(1, Math.min(limit, 500)); + const rows = await this.prisma.$queryRaw` + SELECT id, queue_name, body->'message'->>'name' AS message_name, + created_at, available_at, delivered_at + FROM queue.messages ORDER BY created_at DESC LIMIT ${clampedLimit} + `; + + return rows.map((row) => ({ + id: String(row.id), + queueName: row.queue_name, + messageName: row.message_name, + status: deriveMessageStatus(row), + createdAt: row.created_at, + })); + } + + async getWorkers(): Promise { + const rows = await this.prisma.$queryRaw` + SELECT worker_id, started_at, last_seen_at + FROM queue.worker_heartbeats ORDER BY started_at DESC + `; + + return rows.map((row) => ({ + workerId: row.worker_id, + status: deriveWorkerStatus(row.last_seen_at), + startedAt: row.started_at, + lastSeenAt: row.last_seen_at, + })); + } +} diff --git a/packages/core/src/modules/app/app.module.ts b/packages/core/src/modules/app/app.module.ts new file mode 100644 index 0000000..7715df5 --- /dev/null +++ b/packages/core/src/modules/app/app.module.ts @@ -0,0 +1,10 @@ +import { Module } from "@nestjs/common"; +import { AppService } from "./app.service"; +import { HealthController } from "./health.controller"; + +@Module({ + controllers: [HealthController], + providers: [AppService], + exports: [AppService], +}) +export class AppModule {} diff --git a/packages/core/src/modules/app/app.service.ts b/packages/core/src/modules/app/app.service.ts new file mode 100644 index 0000000..fc11559 --- /dev/null +++ b/packages/core/src/modules/app/app.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from "@nestjs/common"; + +export interface HealthResponse { + status: string; + timestamp: string; + timezone: string; + uptime: number; +} + +@Injectable() +export class AppService { + private readonly startTime = Date.now(); + + getHealth(): HealthResponse { + const now = new Date(); + return { + status: "ok", + timestamp: now.toISOString(), + timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, + uptime: Date.now() - this.startTime, + }; + } +} diff --git a/packages/core/src/modules/app/health.controller.ts b/packages/core/src/modules/app/health.controller.ts new file mode 100644 index 0000000..d29dbcd --- /dev/null +++ b/packages/core/src/modules/app/health.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from "@nestjs/common"; +import { AppService } from "./app.service"; + +@Controller("health") +export class HealthController { + constructor(private readonly appService: AppService) {} + + @Get() + check() { + return this.appService.getHealth(); + } +} diff --git a/packages/core/src/modules/application/application-status/application-status.dto.ts b/packages/core/src/modules/application/application-status/application-status.dto.ts new file mode 100644 index 0000000..7e12d1f --- /dev/null +++ b/packages/core/src/modules/application/application-status/application-status.dto.ts @@ -0,0 +1,9 @@ +export interface CreateApplicationStatusDto { + name: string; + description?: string; +} + +export interface UpdateApplicationStatusDto { + name?: string; + description?: string; +} diff --git a/packages/core/src/modules/application/application-status/application-status.entity.ts b/packages/core/src/modules/application/application-status/application-status.entity.ts new file mode 100644 index 0000000..d5b5a81 --- /dev/null +++ b/packages/core/src/modules/application/application-status/application-status.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "../../../shared"; + +export class ApplicationStatus extends NamedEntity {} diff --git a/packages/core/src/modules/application/application-status/application-status.factory.ts b/packages/core/src/modules/application/application-status/application-status.factory.ts new file mode 100644 index 0000000..43a474c --- /dev/null +++ b/packages/core/src/modules/application/application-status/application-status.factory.ts @@ -0,0 +1,25 @@ +import { ClockService, Factory, UuidFactoryService } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { CreateApplicationStatusDto } from "./application-status.dto"; +import { ApplicationStatus } from "./application-status.entity"; + +@Injectable() +export class ApplicationStatusFactory + implements Factory +{ + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(dto: CreateApplicationStatusDto): ApplicationStatus { + const now = this.clock.now(); + return new ApplicationStatus( + this.uuidFactory.generate(), + dto.name, + now, + now, + dto.description, + ); + } +} diff --git a/packages/core/src/modules/application/application-status/application-status.mapper.ts b/packages/core/src/modules/application/application-status/application-status.mapper.ts new file mode 100644 index 0000000..49d0aa6 --- /dev/null +++ b/packages/core/src/modules/application/application-status/application-status.mapper.ts @@ -0,0 +1,29 @@ +import { createNamedEntityMapper } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { ApplicationStatus as PrismaApplicationStatus } from "@prisma/client"; +import { ApplicationStatus } from "./application-status.entity"; + +@Injectable() +export class ApplicationStatusMapper { + private mapper = createNamedEntityMapper< + PrismaApplicationStatus, + ApplicationStatus + >(ApplicationStatus); + + toDomain(prismaEntity: null): null; + toDomain(prismaEntity: PrismaApplicationStatus): ApplicationStatus; + toDomain( + prismaEntity: PrismaApplicationStatus | null, + ): ApplicationStatus | null; + toDomain( + prismaEntity: PrismaApplicationStatus | null, + ): ApplicationStatus | null { + return this.mapper.toDomain(prismaEntity); + } + + mapToDomain( + prismaEntities: PrismaApplicationStatus[], + ): ApplicationStatus[] { + return this.mapper.mapToDomain(prismaEntities); + } +} diff --git a/packages/core/src/modules/application/application-status/application-status.module.ts b/packages/core/src/modules/application/application-status/application-status.module.ts new file mode 100644 index 0000000..6988736 --- /dev/null +++ b/packages/core/src/modules/application/application-status/application-status.module.ts @@ -0,0 +1,21 @@ +import { BaseModule } from "../../../shared"; +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { ApplicationStatusFactory } from "./application-status.factory"; +import { ApplicationStatusMapper } from "./application-status.mapper"; +import { ApplicationStatusService } from "./application-status.service"; + +@Module({ + imports: [DatabaseModule, BaseModule], + providers: [ + ApplicationStatusService, + ApplicationStatusMapper, + ApplicationStatusFactory, + ], + exports: [ + ApplicationStatusService, + ApplicationStatusMapper, + ApplicationStatusFactory, + ], +}) +export class ApplicationStatusModule {} diff --git a/packages/core/src/modules/application/application-status/application-status.service.ts b/packages/core/src/modules/application/application-status/application-status.service.ts new file mode 100644 index 0000000..2783096 --- /dev/null +++ b/packages/core/src/modules/application/application-status/application-status.service.ts @@ -0,0 +1,20 @@ +import { NamedEntityService } from "../../../shared"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { ApplicationStatus } from "./application-status.entity"; +import { ApplicationStatusMapper } from "./application-status.mapper"; + +@Injectable() +export class ApplicationStatusService extends NamedEntityService { + constructor( + prisma: PrismaService, + applicationStatusMapper: ApplicationStatusMapper, + ) { + super( + prisma, + applicationStatusMapper, + prisma.applicationStatus, + "ApplicationStatus", + ); + } +} diff --git a/packages/core/src/modules/application/application.entity.ts b/packages/core/src/modules/application/application.entity.ts new file mode 100644 index 0000000..63d844a --- /dev/null +++ b/packages/core/src/modules/application/application.entity.ts @@ -0,0 +1,49 @@ +import { BaseEntity } from "../../shared"; +import { CV } from "../cv-template/cv.entity"; +import { Vacancy } from "../vacancies/vacancy.entity"; + +export interface ApplicationStatusRelation { + id: string; + name: string; + description: string | null; + createdAt: Date; + updatedAt: Date; +} + +export class Application extends BaseEntity { + userId: string; + profileId: string; + vacancy: Vacancy; + cv: CV | null; + coverLetter?: string; + statusId: string; + status: ApplicationStatusRelation; + appliedAt: Date; + + constructor( + id: string, + userId: string, + profileId: string, + vacancy: Vacancy, + statusId: string, + status: ApplicationStatusRelation, + appliedAt: Date, + createdAt: Date, + updatedAt: Date, + cv: CV | null = null, + coverLetter?: string, + ) { + super(id, createdAt, updatedAt); + this.userId = userId; + this.profileId = profileId; + this.vacancy = vacancy; + this.statusId = statusId; + this.status = status; + this.appliedAt = appliedAt; + this.cv = cv; + + if (coverLetter !== undefined) { + this.coverLetter = coverLetter; + } + } +} diff --git a/packages/core/src/modules/application/application.error.ts b/packages/core/src/modules/application/application.error.ts new file mode 100644 index 0000000..5c11754 --- /dev/null +++ b/packages/core/src/modules/application/application.error.ts @@ -0,0 +1,7 @@ +import { DomainError } from "../../shared"; + +export class DuplicateApplicationError extends DomainError { + constructor() { + super("APPLICATION_DUPLICATE", "User has already applied to this vacancy"); + } +} diff --git a/packages/core/src/modules/application/application.mapper.ts b/packages/core/src/modules/application/application.mapper.ts new file mode 100644 index 0000000..8a710e6 --- /dev/null +++ b/packages/core/src/modules/application/application.mapper.ts @@ -0,0 +1,83 @@ +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; +import { cvMapper } from "../cv-template/cv.mapper"; +import { VacancyMapper } from "../vacancies/vacancy.mapper"; +import { + Application, + type ApplicationStatusRelation, +} from "./application.entity"; + +type PrismaApplicationWithRelations = Prisma.ApplicationGetPayload<{ + include: { + user: true; + status: true; + vacancy: { + include: { + company: true; + role: true; + level: true; + jobType: true; + skills: true; + }; + }; + cv: { + include: { + template: true; + }; + }; + }; +}>; + +@Injectable() +export class ApplicationMapper { + constructor(private readonly vacancyMapper: VacancyMapper) {} + + toDomain( + prismaApplication: PrismaApplicationWithRelations | null, + ): Application | null { + if (!prismaApplication) { + return null; + } + + const status: ApplicationStatusRelation = { + id: prismaApplication.status.id, + name: prismaApplication.status.name, + description: prismaApplication.status.description, + createdAt: prismaApplication.status.createdAt, + updatedAt: prismaApplication.status.updatedAt, + }; + + const vacancy = this.vacancyMapper.toDomain(prismaApplication.vacancy); + if (!vacancy) { + throw new Error("Vacancy is required for Application"); + } + + const cv = prismaApplication.cv + ? cvMapper.toDomain(prismaApplication.cv) + : null; + + return new Application( + prismaApplication.id, + prismaApplication.userId, + prismaApplication.profileId, + vacancy, + prismaApplication.statusId, + status, + prismaApplication.appliedAt, + prismaApplication.createdAt, + prismaApplication.updatedAt, + cv, + prismaApplication.coverLetter ?? undefined, + ); + } + + mapToDomainWithRelations( + prismaApplications: PrismaApplicationWithRelations[], + ): Application[] { + return prismaApplications + .map((application) => this.toDomain(application)) + .filter( + (application): application is Application => application !== null, + ); + } +} diff --git a/packages/core/src/modules/application/application.module.ts b/packages/core/src/modules/application/application.module.ts new file mode 100644 index 0000000..ee31e2c --- /dev/null +++ b/packages/core/src/modules/application/application.module.ts @@ -0,0 +1,26 @@ +import { AuthorizationModule } from "../auth"; +import { DatabaseModule } from "../database"; +import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "../authentication/authentication.module"; +import { CVTemplateModule } from "../cv-template/cv-template.module"; +import { VacancyModule } from "../vacancies/vacancy.module"; +import { ApplicationMapper } from "./application.mapper"; +import { ApplicationPolicy } from "./application.policy"; +import { ApplicationService } from "./application.service"; + +@Module({ + imports: [ + DatabaseModule, + AuthenticationModule, + AuthorizationModule, + VacancyModule, + CVTemplateModule, + ], + providers: [ + ApplicationService, + ApplicationMapper, + ApplicationPolicy, + ], + exports: [ApplicationService, ApplicationMapper], +}) +export class ApplicationModule {} diff --git a/packages/core/src/modules/application/application.policy.ts b/packages/core/src/modules/application/application.policy.ts new file mode 100644 index 0000000..9d9773b --- /dev/null +++ b/packages/core/src/modules/application/application.policy.ts @@ -0,0 +1,7 @@ +import { Policy, UserOwnedResourcePolicy } from "../auth"; +import { Injectable } from "@nestjs/common"; +import { Application } from "./application.entity"; + +@Injectable() +@Policy(Application) +export class ApplicationPolicy extends UserOwnedResourcePolicy {} diff --git a/packages/core/src/modules/application/application.service.ts b/packages/core/src/modules/application/application.service.ts new file mode 100644 index 0000000..13502f5 --- /dev/null +++ b/packages/core/src/modules/application/application.service.ts @@ -0,0 +1,143 @@ +import { notFound } from "../auth"; +import { type EntityService } from "../../shared"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import { Prisma } from "@prisma/client"; +import { Application } from "./application.entity"; +import { DuplicateApplicationError } from "./application.error"; +import { ApplicationMapper } from "./application.mapper"; + +type ApplicationFilters = { + userId?: string; + id?: string | string[]; +}; + +type ApplicationWhereInput = { + userId?: string; + id?: { in: string[] } | string; +}; + +@Injectable() +export class ApplicationService + implements EntityService +{ + readonly applicationInclude = { + user: true, + vacancy: { + include: { + company: true, + role: true, + level: true, + jobType: true, + skills: true, + }, + }, + status: true, + cv: { + include: { + template: true, + }, + }, + } satisfies Prisma.ApplicationInclude; + + constructor( + private readonly prisma: PrismaService, + private readonly applicationMapper: ApplicationMapper, + ) {} + + async findById(id: string): Promise { + const application = await this.prisma.application.findUnique({ + where: { id }, + include: this.applicationInclude, + }); + + return application ? this.applicationMapper.toDomain(application) : null; + } + + async findByIdOrFail(id: string): Promise { + const application = await this.findById(id); + return application ?? notFound("Application", "id", id); + } + + private buildWhere(filters: ApplicationFilters = {}): ApplicationWhereInput { + const where: ApplicationWhereInput = {}; + if (filters.userId) { + where.userId = filters.userId; + } + if (filters.id !== undefined) { + where.id = Array.isArray(filters.id) ? { in: filters.id } : filters.id; + } + return where; + } + + async findMany(filters: ApplicationFilters = {}): Promise { + const applications = await this.prisma.application.findMany({ + where: this.buildWhere(filters), + include: this.applicationInclude, + orderBy: { appliedAt: "desc" }, + }); + + return this.applicationMapper.mapToDomainWithRelations(applications); + } + + async count(filters: ApplicationFilters = {}): Promise { + return this.prisma.application.count({ where: this.buildWhere(filters) }); + } + + private buildPrismaData(entity: Application): { + statusId: string; + cvId: string | null; + coverLetter: string | null; + } { + return { + statusId: entity.statusId, + cvId: entity.cv?.id ?? null, + coverLetter: entity.coverLetter ?? null, + }; + } + + async save(entity: Application): Promise { + const data = this.buildPrismaData(entity); + + try { + const application = await this.prisma.application.upsert({ + where: { id: entity.id }, + create: { + id: entity.id, + userId: entity.userId, + profileId: entity.profileId, + vacancyId: entity.vacancy.id, + ...data, + }, + update: data, + include: this.applicationInclude, + }); + + const domain = this.applicationMapper.toDomain(application); + if (!domain) { + throw new Error("Failed to map application to domain"); + } + return domain; + } catch (error: unknown) { + if ( + error instanceof Prisma.PrismaClientKnownRequestError && + error.code === "P2002" && + error.meta && + typeof error.meta === "object" && + "target" in error.meta && + Array.isArray(error.meta["target"]) && + error.meta["target"].includes("userId") && + error.meta["target"].includes("vacancyId") + ) { + throw new DuplicateApplicationError(); + } + throw error; + } + } + + async destroy(entity: Application): Promise { + await this.prisma.application.delete({ + where: { id: entity.id }, + }); + } +} diff --git a/packages/auth/src/authorization/authorization.module.ts b/packages/core/src/modules/auth/authorization/authorization.module.ts similarity index 100% rename from packages/auth/src/authorization/authorization.module.ts rename to packages/core/src/modules/auth/authorization/authorization.module.ts diff --git a/packages/auth/src/authorization/authorization.service.ts b/packages/core/src/modules/auth/authorization/authorization.service.ts similarity index 97% rename from packages/auth/src/authorization/authorization.service.ts rename to packages/core/src/modules/auth/authorization/authorization.service.ts index 7a5f79c..6f54ac3 100644 --- a/packages/auth/src/authorization/authorization.service.ts +++ b/packages/core/src/modules/auth/authorization/authorization.service.ts @@ -1,4 +1,4 @@ -import type { BaseEntity } from "@cv/system"; +import type { BaseEntity } from "../../../shared"; import { Injectable, type Type } from "@nestjs/common"; import { CannotCreateError, diff --git a/packages/auth/src/authorization/index.ts b/packages/core/src/modules/auth/authorization/index.ts similarity index 84% rename from packages/auth/src/authorization/index.ts rename to packages/core/src/modules/auth/authorization/index.ts index cba2193..503f7af 100644 --- a/packages/auth/src/authorization/index.ts +++ b/packages/core/src/modules/auth/authorization/index.ts @@ -1,7 +1,7 @@ export * from "./authorization.module"; export * from "./authorization.service"; export * from "./owner-owned-resource.policy"; -export { Policy } from "./policy.decorator"; +export { Policy, POLICY_RESOURCE_KEY } from "./policy.decorator"; export type { Policy as IPolicy } from "./policy.interface"; export * from "./policy-registry.service"; export * from "./profile-owned-resource.policy"; diff --git a/packages/auth/src/authorization/owner-owned-resource.policy.ts b/packages/core/src/modules/auth/authorization/owner-owned-resource.policy.ts similarity index 100% rename from packages/auth/src/authorization/owner-owned-resource.policy.ts rename to packages/core/src/modules/auth/authorization/owner-owned-resource.policy.ts diff --git a/packages/auth/src/authorization/policy-registry.service.ts b/packages/core/src/modules/auth/authorization/policy-registry.service.ts similarity index 97% rename from packages/auth/src/authorization/policy-registry.service.ts rename to packages/core/src/modules/auth/authorization/policy-registry.service.ts index e1fd270..88d510e 100644 --- a/packages/auth/src/authorization/policy-registry.service.ts +++ b/packages/core/src/modules/auth/authorization/policy-registry.service.ts @@ -1,4 +1,4 @@ -import { raise } from "@cv/system"; +import { raise } from "../../../shared"; import { Injectable, type OnModuleInit, type Type } from "@nestjs/common"; import { DiscoveryService, Reflector } from "@nestjs/core"; import { POLICY_RESOURCE_KEY } from "./policy.decorator"; diff --git a/packages/auth/src/authorization/policy.decorator.ts b/packages/core/src/modules/auth/authorization/policy.decorator.ts similarity index 100% rename from packages/auth/src/authorization/policy.decorator.ts rename to packages/core/src/modules/auth/authorization/policy.decorator.ts diff --git a/packages/auth/src/authorization/policy.interface.ts b/packages/core/src/modules/auth/authorization/policy.interface.ts similarity index 100% rename from packages/auth/src/authorization/policy.interface.ts rename to packages/core/src/modules/auth/authorization/policy.interface.ts diff --git a/packages/auth/src/authorization/profile-owned-resource.policy.ts b/packages/core/src/modules/auth/authorization/profile-owned-resource.policy.ts similarity index 95% rename from packages/auth/src/authorization/profile-owned-resource.policy.ts rename to packages/core/src/modules/auth/authorization/profile-owned-resource.policy.ts index e49c39a..b8cc982 100644 --- a/packages/auth/src/authorization/profile-owned-resource.policy.ts +++ b/packages/core/src/modules/auth/authorization/profile-owned-resource.policy.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "../../database"; import type { User } from "../user/user.entity"; import type { Policy } from "./policy.interface"; diff --git a/packages/auth/src/authorization/public-resource.policy.ts b/packages/core/src/modules/auth/authorization/public-resource.policy.ts similarity index 100% rename from packages/auth/src/authorization/public-resource.policy.ts rename to packages/core/src/modules/auth/authorization/public-resource.policy.ts diff --git a/packages/auth/src/authorization/user-owned-resource.policy.ts b/packages/core/src/modules/auth/authorization/user-owned-resource.policy.ts similarity index 100% rename from packages/auth/src/authorization/user-owned-resource.policy.ts rename to packages/core/src/modules/auth/authorization/user-owned-resource.policy.ts diff --git a/packages/auth/src/config/index.ts b/packages/core/src/modules/auth/config/index.ts similarity index 100% rename from packages/auth/src/config/index.ts rename to packages/core/src/modules/auth/config/index.ts diff --git a/packages/auth/src/config/jwt.config.ts b/packages/core/src/modules/auth/config/jwt.config.ts similarity index 69% rename from packages/auth/src/config/jwt.config.ts rename to packages/core/src/modules/auth/config/jwt.config.ts index 58a77d9..1cdc05e 100644 --- a/packages/auth/src/config/jwt.config.ts +++ b/packages/core/src/modules/auth/config/jwt.config.ts @@ -13,6 +13,14 @@ export class JwtConfigService { return this.configService.getOrThrow("JWT_REFRESH_TOKEN_EXPIRY"); } + getAccessTokenExpirySeconds(): number { + return this.parseToSeconds(this.getAccessTokenExpiry()); + } + + getRefreshTokenExpirySeconds(): number { + return this.parseToSeconds(this.getRefreshTokenExpiry()); + } + getSecret(): string { return this.configService.getOrThrow("JWT_SECRET"); } @@ -50,4 +58,21 @@ export class JwtConfigService { calculateAccessTokenExpiryDate(): Date { return this.calculateExpiryDate(this.getAccessTokenExpiry()); } + + private parseToSeconds(expiryString: string): number { + const match = expiryString.match(/^(\d+)([smhd])$/); + if (!(match?.[1] && match[2])) { + return 900; + } + + const value = Number.parseInt(match[1], 10); + const multipliers: Record = { + s: 1, + m: 60, + h: 3600, + d: 86400, + }; + + return value * (multipliers[match[2]] ?? 60); + } } diff --git a/packages/auth/src/cookie/cookie.service.ts b/packages/core/src/modules/auth/cookie/cookie.service.ts similarity index 100% rename from packages/auth/src/cookie/cookie.service.ts rename to packages/core/src/modules/auth/cookie/cookie.service.ts diff --git a/packages/auth/src/cookie/index.ts b/packages/core/src/modules/auth/cookie/index.ts similarity index 100% rename from packages/auth/src/cookie/index.ts rename to packages/core/src/modules/auth/cookie/index.ts diff --git a/packages/auth/src/errors/authentication.error.ts b/packages/core/src/modules/auth/errors/authentication.error.ts similarity index 98% rename from packages/auth/src/errors/authentication.error.ts rename to packages/core/src/modules/auth/errors/authentication.error.ts index 41056ee..18d704c 100644 --- a/packages/auth/src/errors/authentication.error.ts +++ b/packages/core/src/modules/auth/errors/authentication.error.ts @@ -1,4 +1,4 @@ -import { DomainError } from "@cv/system"; +import { DomainError } from "../../../shared"; export abstract class AuthenticationError extends DomainError {} diff --git a/packages/auth/src/errors/authorization.error.ts b/packages/core/src/modules/auth/errors/authorization.error.ts similarity index 95% rename from packages/auth/src/errors/authorization.error.ts rename to packages/core/src/modules/auth/errors/authorization.error.ts index 6f46a56..f1e2f3e 100644 --- a/packages/auth/src/errors/authorization.error.ts +++ b/packages/core/src/modules/auth/errors/authorization.error.ts @@ -1,4 +1,4 @@ -import { DomainError } from "@cv/system"; +import { DomainError } from "../../../shared"; export abstract class AuthorizationError extends DomainError {} diff --git a/packages/auth/src/errors/index.ts b/packages/core/src/modules/auth/errors/index.ts similarity index 100% rename from packages/auth/src/errors/index.ts rename to packages/core/src/modules/auth/errors/index.ts diff --git a/packages/auth/src/errors/not-found.util.ts b/packages/core/src/modules/auth/errors/not-found.util.ts similarity index 90% rename from packages/auth/src/errors/not-found.util.ts rename to packages/core/src/modules/auth/errors/not-found.util.ts index 9c7ceca..2a08d6c 100644 --- a/packages/auth/src/errors/not-found.util.ts +++ b/packages/core/src/modules/auth/errors/not-found.util.ts @@ -1,4 +1,4 @@ -import { DomainError } from "@cv/system"; +import { DomainError } from "../../../shared"; export class EntityNotFoundError extends DomainError { constructor(entityName: string, property: string, value: string) { diff --git a/packages/auth/src/identity-provider-registry.service.ts b/packages/core/src/modules/auth/identity-provider-registry.service.ts similarity index 99% rename from packages/auth/src/identity-provider-registry.service.ts rename to packages/core/src/modules/auth/identity-provider-registry.service.ts index e435aaa..ee2479e 100644 --- a/packages/auth/src/identity-provider-registry.service.ts +++ b/packages/core/src/modules/auth/identity-provider-registry.service.ts @@ -1,4 +1,4 @@ -import { raise } from "@cv/system"; +import { raise } from "../../shared"; import { Injectable, type OnModuleInit } from "@nestjs/common"; import { DiscoveryService, Reflector } from "@nestjs/core"; import { z } from "zod/v4"; diff --git a/packages/auth/src/identity-provider.decorator.ts b/packages/core/src/modules/auth/identity-provider.decorator.ts similarity index 100% rename from packages/auth/src/identity-provider.decorator.ts rename to packages/core/src/modules/auth/identity-provider.decorator.ts diff --git a/packages/auth/src/identity-provider.interface.ts b/packages/core/src/modules/auth/identity-provider.interface.ts similarity index 100% rename from packages/auth/src/identity-provider.interface.ts rename to packages/core/src/modules/auth/identity-provider.interface.ts diff --git a/packages/core/src/modules/auth/index.ts b/packages/core/src/modules/auth/index.ts new file mode 100644 index 0000000..844533d --- /dev/null +++ b/packages/core/src/modules/auth/index.ts @@ -0,0 +1,17 @@ +export * from "./authorization"; +export * from "./config"; +export * from "./cookie"; +export * from "./errors"; +export { + IDENTITY_PROVIDER_KEY, + IdentityProvider as IdentityProviderDecorator, + type IdentityProviderMeta, +} from "./identity-provider.decorator"; +export type { IdentityProvider } from "./identity-provider.interface"; +export * from "./identity-provider-registry.service"; +export * from "./jwt"; +export * from "./metadata"; +export * from "./providers"; +export * from "./request"; +export * from "./token"; +export * from "./user"; diff --git a/packages/auth/src/jwt/index.ts b/packages/core/src/modules/auth/jwt/index.ts similarity index 100% rename from packages/auth/src/jwt/index.ts rename to packages/core/src/modules/auth/jwt/index.ts diff --git a/packages/auth/src/jwt/jwt-scope.enum.ts b/packages/core/src/modules/auth/jwt/jwt-scope.enum.ts similarity index 100% rename from packages/auth/src/jwt/jwt-scope.enum.ts rename to packages/core/src/modules/auth/jwt/jwt-scope.enum.ts diff --git a/packages/auth/src/metadata/device-identification.service.ts b/packages/core/src/modules/auth/metadata/device-identification.service.ts similarity index 100% rename from packages/auth/src/metadata/device-identification.service.ts rename to packages/core/src/modules/auth/metadata/device-identification.service.ts diff --git a/packages/auth/src/metadata/index.ts b/packages/core/src/modules/auth/metadata/index.ts similarity index 100% rename from packages/auth/src/metadata/index.ts rename to packages/core/src/modules/auth/metadata/index.ts diff --git a/packages/auth/src/metadata/location.service.ts b/packages/core/src/modules/auth/metadata/location.service.ts similarity index 100% rename from packages/auth/src/metadata/location.service.ts rename to packages/core/src/modules/auth/metadata/location.service.ts diff --git a/packages/auth/src/providers/index.ts b/packages/core/src/modules/auth/providers/index.ts similarity index 100% rename from packages/auth/src/providers/index.ts rename to packages/core/src/modules/auth/providers/index.ts diff --git a/packages/auth/src/providers/password/credentials/email.config.ts b/packages/core/src/modules/auth/providers/password/credentials/email.config.ts similarity index 100% rename from packages/auth/src/providers/password/credentials/email.config.ts rename to packages/core/src/modules/auth/providers/password/credentials/email.config.ts diff --git a/packages/auth/src/providers/password/credentials/emails/templates/password-reset-requested-html.hbs b/packages/core/src/modules/auth/providers/password/credentials/emails/templates/password-reset-requested-html.hbs similarity index 100% rename from packages/auth/src/providers/password/credentials/emails/templates/password-reset-requested-html.hbs rename to packages/core/src/modules/auth/providers/password/credentials/emails/templates/password-reset-requested-html.hbs diff --git a/packages/auth/src/providers/password/credentials/emails/templates/password-reset-requested-subject.hbs b/packages/core/src/modules/auth/providers/password/credentials/emails/templates/password-reset-requested-subject.hbs similarity index 100% rename from packages/auth/src/providers/password/credentials/emails/templates/password-reset-requested-subject.hbs rename to packages/core/src/modules/auth/providers/password/credentials/emails/templates/password-reset-requested-subject.hbs diff --git a/packages/auth/src/providers/password/credentials/emails/templates/password-reset-requested-text.hbs b/packages/core/src/modules/auth/providers/password/credentials/emails/templates/password-reset-requested-text.hbs similarity index 100% rename from packages/auth/src/providers/password/credentials/emails/templates/password-reset-requested-text.hbs rename to packages/core/src/modules/auth/providers/password/credentials/emails/templates/password-reset-requested-text.hbs diff --git a/packages/auth/src/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-html.hbs b/packages/core/src/modules/auth/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-html.hbs similarity index 100% rename from packages/auth/src/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-html.hbs rename to packages/core/src/modules/auth/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-html.hbs diff --git a/packages/auth/src/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-subject.hbs b/packages/core/src/modules/auth/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-subject.hbs similarity index 100% rename from packages/auth/src/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-subject.hbs rename to packages/core/src/modules/auth/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-subject.hbs diff --git a/packages/auth/src/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-text.hbs b/packages/core/src/modules/auth/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-text.hbs similarity index 100% rename from packages/auth/src/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-text.hbs rename to packages/core/src/modules/auth/providers/password/credentials/emails/templates/registration-attempt-on-existing-email-text.hbs diff --git a/packages/auth/src/providers/password/credentials/emails/templates/verification-email-requested-html.hbs b/packages/core/src/modules/auth/providers/password/credentials/emails/templates/verification-email-requested-html.hbs similarity index 100% rename from packages/auth/src/providers/password/credentials/emails/templates/verification-email-requested-html.hbs rename to packages/core/src/modules/auth/providers/password/credentials/emails/templates/verification-email-requested-html.hbs diff --git a/packages/auth/src/providers/password/credentials/emails/templates/verification-email-requested-subject.hbs b/packages/core/src/modules/auth/providers/password/credentials/emails/templates/verification-email-requested-subject.hbs similarity index 100% rename from packages/auth/src/providers/password/credentials/emails/templates/verification-email-requested-subject.hbs rename to packages/core/src/modules/auth/providers/password/credentials/emails/templates/verification-email-requested-subject.hbs diff --git a/packages/auth/src/providers/password/credentials/emails/templates/verification-email-requested-text.hbs b/packages/core/src/modules/auth/providers/password/credentials/emails/templates/verification-email-requested-text.hbs similarity index 100% rename from packages/auth/src/providers/password/credentials/emails/templates/verification-email-requested-text.hbs rename to packages/core/src/modules/auth/providers/password/credentials/emails/templates/verification-email-requested-text.hbs diff --git a/packages/core/src/modules/auth/providers/password/credentials/events/index.ts b/packages/core/src/modules/auth/providers/password/credentials/events/index.ts new file mode 100644 index 0000000..86b2ae0 --- /dev/null +++ b/packages/core/src/modules/auth/providers/password/credentials/events/index.ts @@ -0,0 +1,3 @@ +export { PasswordResetRequestedEvent } from "./password-reset-requested.event"; +export { RegistrationAttemptOnExistingEmailEvent } from "./registration-attempt-on-existing-email.event"; +export { VerificationEmailRequestedEvent } from "./verification-email-requested.event"; diff --git a/packages/core/src/modules/auth/providers/password/credentials/events/password-reset-requested.event.ts b/packages/core/src/modules/auth/providers/password/credentials/events/password-reset-requested.event.ts new file mode 100644 index 0000000..24d58a2 --- /dev/null +++ b/packages/core/src/modules/auth/providers/password/credentials/events/password-reset-requested.event.ts @@ -0,0 +1,8 @@ +import { DomainEvent } from "../../../../../events"; + +interface PasswordResetRequestedPayload { + email: string; + resetToken: string; +} + +export class PasswordResetRequestedEvent extends DomainEvent {} diff --git a/packages/core/src/modules/auth/providers/password/credentials/events/registration-attempt-on-existing-email.event.ts b/packages/core/src/modules/auth/providers/password/credentials/events/registration-attempt-on-existing-email.event.ts new file mode 100644 index 0000000..0d29e37 --- /dev/null +++ b/packages/core/src/modules/auth/providers/password/credentials/events/registration-attempt-on-existing-email.event.ts @@ -0,0 +1,7 @@ +import { DomainEvent } from "../../../../../events"; + +interface RegistrationAttemptOnExistingEmailPayload { + email: string; +} + +export class RegistrationAttemptOnExistingEmailEvent extends DomainEvent {} diff --git a/packages/core/src/modules/auth/providers/password/credentials/events/verification-email-requested.event.ts b/packages/core/src/modules/auth/providers/password/credentials/events/verification-email-requested.event.ts new file mode 100644 index 0000000..182b0c8 --- /dev/null +++ b/packages/core/src/modules/auth/providers/password/credentials/events/verification-email-requested.event.ts @@ -0,0 +1,9 @@ +import { DomainEvent } from "../../../../../events"; + +interface VerificationEmailRequestedPayload { + userId: string; + email: string; + verificationToken: string; +} + +export class VerificationEmailRequestedEvent extends DomainEvent {} diff --git a/packages/auth/src/providers/password/credentials/index.ts b/packages/core/src/modules/auth/providers/password/credentials/index.ts similarity index 100% rename from packages/auth/src/providers/password/credentials/index.ts rename to packages/core/src/modules/auth/providers/password/credentials/index.ts diff --git a/packages/auth/src/providers/password/credentials/listeners/index.ts b/packages/core/src/modules/auth/providers/password/credentials/listeners/index.ts similarity index 100% rename from packages/auth/src/providers/password/credentials/listeners/index.ts rename to packages/core/src/modules/auth/providers/password/credentials/listeners/index.ts diff --git a/packages/auth/src/providers/password/credentials/listeners/password-reset-email.listener.ts b/packages/core/src/modules/auth/providers/password/credentials/listeners/password-reset-email.listener.ts similarity index 65% rename from packages/auth/src/providers/password/credentials/listeners/password-reset-email.listener.ts rename to packages/core/src/modules/auth/providers/password/credentials/listeners/password-reset-email.listener.ts index 0c29306..9585d27 100644 --- a/packages/auth/src/providers/password/credentials/listeners/password-reset-email.listener.ts +++ b/packages/core/src/modules/auth/providers/password/credentials/listeners/password-reset-email.listener.ts @@ -1,20 +1,17 @@ import { Injectable } from "@nestjs/common"; import { OnEvent } from "@nestjs/event-emitter"; -import { - PASSWORD_RESET_REQUESTED, - type PasswordResetRequestedEvent, -} from "../events/password-reset-requested.event"; +import { PasswordResetRequestedEvent } from "../events/password-reset-requested.event"; import { TemplatedEmailService } from "../templated-email.service"; @Injectable() export class PasswordResetEmailListener { constructor(private readonly templatedEmailService: TemplatedEmailService) {} - @OnEvent(PASSWORD_RESET_REQUESTED) - async handlePasswordResetRequested({ - email, - resetToken, - }: PasswordResetRequestedEvent): Promise { + @OnEvent(PasswordResetRequestedEvent.eventName) + async handlePasswordResetRequested( + event: PasswordResetRequestedEvent, + ): Promise { + const { email, resetToken } = event.payload; await this.templatedEmailService.send({ to: email, data: { diff --git a/packages/auth/src/providers/password/credentials/listeners/registration-attempt-email.listener.ts b/packages/core/src/modules/auth/providers/password/credentials/listeners/registration-attempt-email.listener.ts similarity index 63% rename from packages/auth/src/providers/password/credentials/listeners/registration-attempt-email.listener.ts rename to packages/core/src/modules/auth/providers/password/credentials/listeners/registration-attempt-email.listener.ts index df66c94..35b5a92 100644 --- a/packages/auth/src/providers/password/credentials/listeners/registration-attempt-email.listener.ts +++ b/packages/core/src/modules/auth/providers/password/credentials/listeners/registration-attempt-email.listener.ts @@ -1,19 +1,17 @@ import { Injectable } from "@nestjs/common"; import { OnEvent } from "@nestjs/event-emitter"; -import { - REGISTRATION_ATTEMPT_ON_EXISTING_EMAIL, - type RegistrationAttemptOnExistingEmailEvent, -} from "../events/registration-attempt-on-existing-email.event"; +import { RegistrationAttemptOnExistingEmailEvent } from "../events/registration-attempt-on-existing-email.event"; import { TemplatedEmailService } from "../templated-email.service"; @Injectable() export class RegistrationAttemptEmailListener { constructor(private readonly templatedEmailService: TemplatedEmailService) {} - @OnEvent(REGISTRATION_ATTEMPT_ON_EXISTING_EMAIL) - async handleRegistrationAttemptOnExistingEmail({ - email, - }: RegistrationAttemptOnExistingEmailEvent): Promise { + @OnEvent(RegistrationAttemptOnExistingEmailEvent.eventName) + async handleRegistrationAttemptOnExistingEmail( + event: RegistrationAttemptOnExistingEmailEvent, + ): Promise { + const { email } = event.payload; await this.templatedEmailService.send({ to: email, data: { diff --git a/packages/auth/src/providers/password/credentials/listeners/verification-email.listener.ts b/packages/core/src/modules/auth/providers/password/credentials/listeners/verification-email.listener.ts similarity index 65% rename from packages/auth/src/providers/password/credentials/listeners/verification-email.listener.ts rename to packages/core/src/modules/auth/providers/password/credentials/listeners/verification-email.listener.ts index 8946e71..dbea37b 100644 --- a/packages/auth/src/providers/password/credentials/listeners/verification-email.listener.ts +++ b/packages/core/src/modules/auth/providers/password/credentials/listeners/verification-email.listener.ts @@ -1,20 +1,17 @@ import { Injectable } from "@nestjs/common"; import { OnEvent } from "@nestjs/event-emitter"; -import { - VERIFICATION_EMAIL_REQUESTED, - type VerificationEmailRequestedEvent, -} from "../events/verification-email-requested.event"; +import { VerificationEmailRequestedEvent } from "../events/verification-email-requested.event"; import { TemplatedEmailService } from "../templated-email.service"; @Injectable() export class VerificationEmailListener { constructor(private readonly templatedEmailService: TemplatedEmailService) {} - @OnEvent(VERIFICATION_EMAIL_REQUESTED) - async handleVerificationEmailRequested({ - email, - verificationToken, - }: VerificationEmailRequestedEvent): Promise { + @OnEvent(VerificationEmailRequestedEvent.eventName) + async handleVerificationEmailRequested( + event: VerificationEmailRequestedEvent, + ): Promise { + const { email, verificationToken } = event.payload; await this.templatedEmailService.send({ to: email, data: { diff --git a/packages/auth/src/providers/password/credentials/templated-email.service.ts b/packages/core/src/modules/auth/providers/password/credentials/templated-email.service.ts similarity index 98% rename from packages/auth/src/providers/password/credentials/templated-email.service.ts rename to packages/core/src/modules/auth/providers/password/credentials/templated-email.service.ts index 78ea78a..625d056 100644 --- a/packages/auth/src/providers/password/credentials/templated-email.service.ts +++ b/packages/core/src/modules/auth/providers/password/credentials/templated-email.service.ts @@ -3,7 +3,7 @@ import { type HandlebarsTemplateService, MAIL_SERVICE_TOKEN, type MailService, -} from "@cv/system"; +} from "../../../../mail"; import { Inject, Injectable } from "@nestjs/common"; import { EMAIL_CONFIG_TOKEN, type EmailConfig } from "./email.config"; diff --git a/packages/auth/src/providers/password/index.ts b/packages/core/src/modules/auth/providers/password/index.ts similarity index 100% rename from packages/auth/src/providers/password/index.ts rename to packages/core/src/modules/auth/providers/password/index.ts diff --git a/packages/auth/src/providers/password/password-authentication.service.ts b/packages/core/src/modules/auth/providers/password/password-authentication.service.ts similarity index 87% rename from packages/auth/src/providers/password/password-authentication.service.ts rename to packages/core/src/modules/auth/providers/password/password-authentication.service.ts index 011d506..9f17080 100644 --- a/packages/auth/src/providers/password/password-authentication.service.ts +++ b/packages/core/src/modules/auth/providers/password/password-authentication.service.ts @@ -1,6 +1,6 @@ import { Injectable } from "@nestjs/common"; -import { EventEmitter2 } from "@nestjs/event-emitter"; import * as bcrypt from "bcryptjs"; +import { EventService } from "../../../events"; import { CurrentPasswordIncorrectError, EmailAlreadyVerifiedError, @@ -17,18 +17,9 @@ import { CredentialsService } from "../../user/credentials.service"; import { generateSecureToken } from "../../user/credentials-token.util"; import type { User } from "../../user/user.entity"; import { UserService } from "../../user/user.service"; -import { - PASSWORD_RESET_REQUESTED, - PasswordResetRequestedEvent, -} from "./credentials/events/password-reset-requested.event"; -import { - REGISTRATION_ATTEMPT_ON_EXISTING_EMAIL, - RegistrationAttemptOnExistingEmailEvent, -} from "./credentials/events/registration-attempt-on-existing-email.event"; -import { - VERIFICATION_EMAIL_REQUESTED, - VerificationEmailRequestedEvent, -} from "./credentials/events/verification-email-requested.event"; +import { PasswordResetRequestedEvent } from "./credentials/events/password-reset-requested.event"; +import { RegistrationAttemptOnExistingEmailEvent } from "./credentials/events/registration-attempt-on-existing-email.event"; +import { VerificationEmailRequestedEvent } from "./credentials/events/verification-email-requested.event"; import { PASSWORD_PROVIDER_NAME } from "./password-identity-provider"; export interface LoginDto { @@ -66,7 +57,7 @@ export class PasswordAuthenticationService { constructor( private userService: UserService, private credentialsService: CredentialsService, - private readonly eventEmitter: EventEmitter2, + private readonly eventService: EventService, private tokenExpiryConfig: TokenExpiryConfigService, private tokenService: TokenService, private identityProviderRegistry: IdentityProviderRegistry, @@ -79,9 +70,8 @@ export class PasswordAuthenticationService { const emailExists = await this.credentialsService.exists(email); if (emailExists) { - this.eventEmitter.emit( - REGISTRATION_ATTEMPT_ON_EXISTING_EMAIL, - new RegistrationAttemptOnExistingEmailEvent(email), + this.eventService.emit( + new RegistrationAttemptOnExistingEmailEvent({ email }), ); const existingUser = @@ -114,9 +104,12 @@ export class PasswordAuthenticationService { verificationToken, expiresAt, ); - this.eventEmitter.emit( - VERIFICATION_EMAIL_REQUESTED, - new VerificationEmailRequestedEvent(user.id, email, verificationToken), + this.eventService.emit( + new VerificationEmailRequestedEvent({ + userId: user.id, + email, + verificationToken, + }), ); const userWithCredentials = await this.userService.findByIdOrFail(user.id); @@ -219,13 +212,12 @@ export class PasswordAuthenticationService { verificationToken, expiresAt, ); - this.eventEmitter.emit( - VERIFICATION_EMAIL_REQUESTED, - new VerificationEmailRequestedEvent( - credentials.userId, + this.eventService.emit( + new VerificationEmailRequestedEvent({ + userId: credentials.userId, email, verificationToken, - ), + }), ); } @@ -272,9 +264,8 @@ export class PasswordAuthenticationService { resetToken, expiresAt, ); - this.eventEmitter.emit( - PASSWORD_RESET_REQUESTED, - new PasswordResetRequestedEvent(email, resetToken), + this.eventService.emit( + new PasswordResetRequestedEvent({ email, resetToken }), ); } diff --git a/packages/auth/src/providers/password/password-identity-provider.ts b/packages/core/src/modules/auth/providers/password/password-identity-provider.ts similarity index 97% rename from packages/auth/src/providers/password/password-identity-provider.ts rename to packages/core/src/modules/auth/providers/password/password-identity-provider.ts index 7eada6f..40f09c4 100644 --- a/packages/auth/src/providers/password/password-identity-provider.ts +++ b/packages/core/src/modules/auth/providers/password/password-identity-provider.ts @@ -1,4 +1,4 @@ -import { IdentityProviderDecorator } from "@cv/auth"; +import { IdentityProviderDecorator } from "../.."; import { Injectable } from "@nestjs/common"; import * as bcrypt from "bcryptjs"; import { InvalidCredentialsError } from "../../errors/authentication.error"; diff --git a/packages/auth/src/providers/password/password-provider.module.ts b/packages/core/src/modules/auth/providers/password/password-provider.module.ts similarity index 95% rename from packages/auth/src/providers/password/password-provider.module.ts rename to packages/core/src/modules/auth/providers/password/password-provider.module.ts index dfa6a01..517f25a 100644 --- a/packages/auth/src/providers/password/password-provider.module.ts +++ b/packages/core/src/modules/auth/providers/password/password-provider.module.ts @@ -3,10 +3,9 @@ import { ResendModule, TemplateModule, TemplateRegistryService, -} from "@cv/system"; +} from "../../../mail"; import { Module, type OnModuleInit } from "@nestjs/common"; import { ConfigModule, ConfigService } from "@nestjs/config"; -import { EventEmitterModule } from "@nestjs/event-emitter"; import { AuthorizationModule } from "../../authorization/authorization.module"; import { TokenModule } from "../../token/token.module"; import { UserModule } from "../../user/user.module"; @@ -21,7 +20,6 @@ import { PasswordIdentityProvider } from "./password-identity-provider"; @Module({ imports: [ ConfigModule, - EventEmitterModule.forRoot(), ResendModule, TemplateModule, UserModule, diff --git a/packages/auth/src/request/index.ts b/packages/core/src/modules/auth/request/index.ts similarity index 100% rename from packages/auth/src/request/index.ts rename to packages/core/src/modules/auth/request/index.ts diff --git a/packages/auth/src/request/request-metadata.decorator.ts b/packages/core/src/modules/auth/request/request-metadata.decorator.ts similarity index 100% rename from packages/auth/src/request/request-metadata.decorator.ts rename to packages/core/src/modules/auth/request/request-metadata.decorator.ts diff --git a/packages/auth/src/token/auth-cookie.service.ts b/packages/core/src/modules/auth/token/auth-cookie.service.ts similarity index 100% rename from packages/auth/src/token/auth-cookie.service.ts rename to packages/core/src/modules/auth/token/auth-cookie.service.ts diff --git a/packages/auth/src/token/index.ts b/packages/core/src/modules/auth/token/index.ts similarity index 100% rename from packages/auth/src/token/index.ts rename to packages/core/src/modules/auth/token/index.ts diff --git a/packages/auth/src/token/refresh-token.entity.ts b/packages/core/src/modules/auth/token/refresh-token.entity.ts similarity index 94% rename from packages/auth/src/token/refresh-token.entity.ts rename to packages/core/src/modules/auth/token/refresh-token.entity.ts index ce886a1..bca11e2 100644 --- a/packages/auth/src/token/refresh-token.entity.ts +++ b/packages/core/src/modules/auth/token/refresh-token.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "../../../shared"; export class RefreshToken extends BaseEntity { constructor( diff --git a/packages/auth/src/token/refresh-token.mapper.ts b/packages/core/src/modules/auth/token/refresh-token.mapper.ts similarity index 100% rename from packages/auth/src/token/refresh-token.mapper.ts rename to packages/core/src/modules/auth/token/refresh-token.mapper.ts diff --git a/packages/auth/src/token/refresh-token.policy.ts b/packages/core/src/modules/auth/token/refresh-token.policy.ts similarity index 100% rename from packages/auth/src/token/refresh-token.policy.ts rename to packages/core/src/modules/auth/token/refresh-token.policy.ts diff --git a/packages/auth/src/token/refresh-token.service.ts b/packages/core/src/modules/auth/token/refresh-token.service.ts similarity index 99% rename from packages/auth/src/token/refresh-token.service.ts rename to packages/core/src/modules/auth/token/refresh-token.service.ts index 895febd..af0a9fd 100644 --- a/packages/auth/src/token/refresh-token.service.ts +++ b/packages/core/src/modules/auth/token/refresh-token.service.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "../../database"; import { Injectable } from "@nestjs/common"; import { InvalidRefreshTokenError, notFound } from "../errors"; import { DeviceIdentificationService } from "../metadata/device-identification.service"; diff --git a/packages/auth/src/token/token-expiry.config.ts b/packages/core/src/modules/auth/token/token-expiry.config.ts similarity index 100% rename from packages/auth/src/token/token-expiry.config.ts rename to packages/core/src/modules/auth/token/token-expiry.config.ts diff --git a/packages/auth/src/token/token.module.ts b/packages/core/src/modules/auth/token/token.module.ts similarity index 96% rename from packages/auth/src/token/token.module.ts rename to packages/core/src/modules/auth/token/token.module.ts index 89e1e83..3d610cb 100644 --- a/packages/auth/src/token/token.module.ts +++ b/packages/core/src/modules/auth/token/token.module.ts @@ -1,4 +1,4 @@ -import { DatabaseModule } from "@cv/system"; +import { DatabaseModule } from "../../database"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { JwtConfigService } from "../config/jwt.config"; diff --git a/packages/auth/src/token/token.service.ts b/packages/core/src/modules/auth/token/token.service.ts similarity index 92% rename from packages/auth/src/token/token.service.ts rename to packages/core/src/modules/auth/token/token.service.ts index 8301c7e..dd3a54b 100644 --- a/packages/auth/src/token/token.service.ts +++ b/packages/core/src/modules/auth/token/token.service.ts @@ -36,12 +36,13 @@ export class TokenService { const tokenScope = scope ?? (isVerified ? JwtScope.VERIFIED : JwtScope.UNVERIFIED); - const accessTokenExpiry = this.jwtConfig.getAccessTokenExpiry(); + const accessTokenExpirySeconds = this.jwtConfig.getAccessTokenExpirySeconds(); const refreshTokenExpiry = this.jwtConfig.getRefreshTokenExpiry(); + const refreshTokenExpirySeconds = this.jwtConfig.getRefreshTokenExpirySeconds(); const refresh_token = this.jwtService.sign( { sub: user.id, type: "refresh" }, - { expiresIn: refreshTokenExpiry }, + { expiresIn: refreshTokenExpirySeconds }, ); const expires_at = this.jwtConfig.calculateAccessTokenExpiryDate(); @@ -64,7 +65,7 @@ export class TokenService { }; const access_token = this.jwtService.sign(payload, { - expiresIn: accessTokenExpiry, + expiresIn: accessTokenExpirySeconds, }); return { access_token, refresh_token, expires_at }; diff --git a/packages/auth/src/user/credentials-token.util.ts b/packages/core/src/modules/auth/user/credentials-token.util.ts similarity index 100% rename from packages/auth/src/user/credentials-token.util.ts rename to packages/core/src/modules/auth/user/credentials-token.util.ts diff --git a/packages/auth/src/user/credentials.entity.ts b/packages/core/src/modules/auth/user/credentials.entity.ts similarity index 92% rename from packages/auth/src/user/credentials.entity.ts rename to packages/core/src/modules/auth/user/credentials.entity.ts index 033b1dc..e1b0be9 100644 --- a/packages/auth/src/user/credentials.entity.ts +++ b/packages/core/src/modules/auth/user/credentials.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "../../../shared"; export class Credentials extends BaseEntity { constructor( diff --git a/packages/auth/src/user/credentials.mapper.ts b/packages/core/src/modules/auth/user/credentials.mapper.ts similarity index 96% rename from packages/auth/src/user/credentials.mapper.ts rename to packages/core/src/modules/auth/user/credentials.mapper.ts index 1ebac24..d6beb5d 100644 --- a/packages/auth/src/user/credentials.mapper.ts +++ b/packages/core/src/modules/auth/user/credentials.mapper.ts @@ -1,4 +1,4 @@ -import type { BaseMapper } from "@cv/system"; +import type { BaseMapper } from "../../../shared"; import { Injectable } from "@nestjs/common"; import type { Credentials as PrismaCredentials } from "@prisma/client"; import { Credentials } from "./credentials.entity"; diff --git a/packages/auth/src/user/credentials.service.ts b/packages/core/src/modules/auth/user/credentials.service.ts similarity index 99% rename from packages/auth/src/user/credentials.service.ts rename to packages/core/src/modules/auth/user/credentials.service.ts index 8daef51..98527d6 100644 --- a/packages/auth/src/user/credentials.service.ts +++ b/packages/core/src/modules/auth/user/credentials.service.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "../../database"; import { Injectable } from "@nestjs/common"; import { notFound } from "../errors"; import { diff --git a/packages/auth/src/user/index.ts b/packages/core/src/modules/auth/user/index.ts similarity index 100% rename from packages/auth/src/user/index.ts rename to packages/core/src/modules/auth/user/index.ts diff --git a/packages/auth/src/user/token-encryption.service.ts b/packages/core/src/modules/auth/user/token-encryption.service.ts similarity index 100% rename from packages/auth/src/user/token-encryption.service.ts rename to packages/core/src/modules/auth/user/token-encryption.service.ts diff --git a/packages/auth/src/user/user.entity.ts b/packages/core/src/modules/auth/user/user.entity.ts similarity index 91% rename from packages/auth/src/user/user.entity.ts rename to packages/core/src/modules/auth/user/user.entity.ts index 8b4a60d..668d9f0 100644 --- a/packages/auth/src/user/user.entity.ts +++ b/packages/core/src/modules/auth/user/user.entity.ts @@ -1,4 +1,4 @@ -import { BaseEntity } from "@cv/system"; +import { BaseEntity } from "../../../shared"; import type { Credentials } from "./credentials.entity"; export enum UserRole { diff --git a/packages/auth/src/user/user.mapper.ts b/packages/core/src/modules/auth/user/user.mapper.ts similarity index 95% rename from packages/auth/src/user/user.mapper.ts rename to packages/core/src/modules/auth/user/user.mapper.ts index 3615dd2..c9011f7 100644 --- a/packages/auth/src/user/user.mapper.ts +++ b/packages/core/src/modules/auth/user/user.mapper.ts @@ -1,4 +1,4 @@ -import type { BaseMapper } from "@cv/system"; +import type { BaseMapper } from "../../../shared"; import { Injectable } from "@nestjs/common"; import type { Prisma } from "@prisma/client"; import { CredentialsMapper } from "./credentials.mapper"; diff --git a/packages/auth/src/user/user.module.ts b/packages/core/src/modules/auth/user/user.module.ts similarity index 93% rename from packages/auth/src/user/user.module.ts rename to packages/core/src/modules/auth/user/user.module.ts index 225d87f..892db8b 100644 --- a/packages/auth/src/user/user.module.ts +++ b/packages/core/src/modules/auth/user/user.module.ts @@ -1,4 +1,4 @@ -import { DatabaseModule } from "@cv/system"; +import { DatabaseModule } from "../../database"; import { Module } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { CredentialsMapper } from "./credentials.mapper"; diff --git a/packages/auth/src/user/user.service.ts b/packages/core/src/modules/auth/user/user.service.ts similarity index 95% rename from packages/auth/src/user/user.service.ts rename to packages/core/src/modules/auth/user/user.service.ts index 72ec106..1e90c4d 100644 --- a/packages/auth/src/user/user.service.ts +++ b/packages/core/src/modules/auth/user/user.service.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "../../database"; import { Injectable } from "@nestjs/common"; import { notFound } from "../errors"; import type { User } from "./user.entity"; diff --git a/packages/core/src/modules/authentication/authentication.module.ts b/packages/core/src/modules/authentication/authentication.module.ts new file mode 100644 index 0000000..afd3923 --- /dev/null +++ b/packages/core/src/modules/authentication/authentication.module.ts @@ -0,0 +1,27 @@ +import { AuthorizationModule } from "../auth/authorization/authorization.module"; +import { TokenModule } from "../auth/token/token.module"; +import { UserModule } from "../auth/user/user.module"; +import { BaseModule } from "../../shared"; +import { DatabaseModule } from "../database"; +import { ResendModule, TemplateModule } from "../mail"; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { AuthenticationService } from "./authentication.service"; +import { PasswordProviderModule } from "./providers/password/password-provider.module"; + +@Module({ + imports: [ + ConfigModule, + UserModule, + TokenModule, + AuthorizationModule, + BaseModule, + DatabaseModule, + ResendModule, + TemplateModule, + PasswordProviderModule, + ], + providers: [AuthenticationService], + exports: [AuthenticationService], +}) +export class AuthenticationModule {} diff --git a/packages/core/src/modules/authentication/authentication.service.ts b/packages/core/src/modules/authentication/authentication.service.ts new file mode 100644 index 0000000..85d8d28 --- /dev/null +++ b/packages/core/src/modules/authentication/authentication.service.ts @@ -0,0 +1,50 @@ +import type { User } from "../auth"; +import { UserService } from "../auth"; +import { Injectable } from "@nestjs/common"; +export interface RefreshTokenDto { + refresh_token: string; +} + +export interface RefreshTokenResponse { + access_token: string; + expires_at: Date; +} + +type InternalRefreshTokenResponse = RefreshTokenResponse & { + refresh_token: string; +}; + +import type { RequestMetadata } from "../auth/request/request-metadata.decorator"; +import { TokenService } from "../auth"; + +@Injectable() +export class AuthenticationService { + constructor( + private userService: UserService, + private tokenService: TokenService, + ) {} + + async refreshToken( + { refresh_token }: RefreshTokenDto, + requestMetadata?: RequestMetadata, + ): Promise { + const tokens = await this.tokenService.refreshTokenPair( + refresh_token, + requestMetadata, + ); + + return { + access_token: tokens.access_token, + expires_at: tokens.expires_at, + refresh_token: tokens.refresh_token, + }; + } + + async validateUser(userId: string): Promise { + try { + return await this.userService.findByIdOrFail(userId); + } catch { + return null; + } + } +} diff --git a/packages/core/src/modules/authentication/providers/password/password-provider.module.ts b/packages/core/src/modules/authentication/providers/password/password-provider.module.ts new file mode 100644 index 0000000..0d4e625 --- /dev/null +++ b/packages/core/src/modules/authentication/providers/password/password-provider.module.ts @@ -0,0 +1,44 @@ +import { + PasswordProviderModule as AuthPasswordProviderModule, + EMAIL_CONFIG_TOKEN, + EmailConfig, + PasswordResetEmailListener, + RegistrationAttemptEmailListener, + TemplatedEmailService, + VerificationEmailListener, +} from "../../../auth"; +import { ResendModule, TemplateModule } from "../../../mail"; +import { Module } from "@nestjs/common"; +import { ConfigModule, ConfigService } from "@nestjs/config"; + +@Module({ + imports: [ + ConfigModule, + AuthPasswordProviderModule, + ResendModule, + TemplateModule, + ], + providers: [ + { + provide: EMAIL_CONFIG_TOKEN, + useFactory: (configService: ConfigService): EmailConfig => { + const clientUrl = + configService.get("CLIENT_URL") ?? + configService.get("CLIENT_ORIGIN") ?? + "http://localhost:5173"; + const fromEmail = + configService.get("EMAIL_FROM_ADDRESS") ?? + "noreply@example.com"; + const fromName = configService.get("EMAIL_FROM_NAME"); + return new EmailConfig(clientUrl, fromEmail, fromName); + }, + inject: [ConfigService], + }, + TemplatedEmailService, + VerificationEmailListener, + PasswordResetEmailListener, + RegistrationAttemptEmailListener, + ], + exports: [], +}) +export class PasswordProviderModule {} diff --git a/packages/core/src/modules/authentication/request/request-with-cookies.type.ts b/packages/core/src/modules/authentication/request/request-with-cookies.type.ts new file mode 100644 index 0000000..a10aade --- /dev/null +++ b/packages/core/src/modules/authentication/request/request-with-cookies.type.ts @@ -0,0 +1,5 @@ +import type { Request } from "express"; + +export type RequestWithCookies> = Request & { + cookies?: TCookies; +}; diff --git a/packages/core/src/modules/authentication/token/token.module.ts b/packages/core/src/modules/authentication/token/token.module.ts new file mode 100644 index 0000000..847b985 --- /dev/null +++ b/packages/core/src/modules/authentication/token/token.module.ts @@ -0,0 +1,8 @@ +import { TokenModule as AuthTokenModule } from "../../auth"; +import { Module } from "@nestjs/common"; + +@Module({ + imports: [AuthTokenModule], + exports: [AuthTokenModule], +}) +export class TokenModule {} diff --git a/packages/core/src/modules/current-user/current-refresh-token-id.decorator.ts b/packages/core/src/modules/current-user/current-refresh-token-id.decorator.ts new file mode 100644 index 0000000..a6194bb --- /dev/null +++ b/packages/core/src/modules/current-user/current-refresh-token-id.decorator.ts @@ -0,0 +1,14 @@ +import { createGqlParamDecorator } from "../../shared"; +import { z } from "zod/v4"; + +const jwtRequestSchema = z.looseObject({ + jwtPayload: z.looseObject({ + refreshTokenId: z.string(), + }), +}); + +export const CurrentRefreshTokenId = createGqlParamDecorator({ + schema: jwtRequestSchema, + lens: ({ jwtPayload: jwt }) => jwt.refreshTokenId, + fallback: undefined, +}); diff --git a/packages/core/src/modules/current-user/current-user.decorator.ts b/packages/core/src/modules/current-user/current-user.decorator.ts new file mode 100644 index 0000000..72c7a67 --- /dev/null +++ b/packages/core/src/modules/current-user/current-user.decorator.ts @@ -0,0 +1,16 @@ +import { createGqlParamDecorator, raise } from "../../shared"; +import { z } from "zod/v4"; + +const requestSchema = z.looseObject({ + user: z.looseObject({ + id: z.string(), + name: z.string(), + role: z.string(), + }), +}); + +export const CurrentUser = createGqlParamDecorator({ + schema: requestSchema, + lens: ({ user }) => user, + fallback: () => raise("User not found in request context"), +}); diff --git a/packages/core/src/modules/current-user/current-user.module.ts b/packages/core/src/modules/current-user/current-user.module.ts new file mode 100644 index 0000000..cede94d --- /dev/null +++ b/packages/core/src/modules/current-user/current-user.module.ts @@ -0,0 +1,10 @@ +import { UserModule } from "../auth"; +import { forwardRef, Module } from "@nestjs/common"; +import { AuthenticationModule } from "../authentication/authentication.module"; + +@Module({ + imports: [UserModule, forwardRef(() => AuthenticationModule)], + providers: [], + exports: [], +}) +export class CurrentUserModule {} diff --git a/packages/core/src/modules/current-user/index.ts b/packages/core/src/modules/current-user/index.ts new file mode 100644 index 0000000..ba0a537 --- /dev/null +++ b/packages/core/src/modules/current-user/index.ts @@ -0,0 +1,2 @@ +export * from "./current-user.decorator"; +export * from "./current-user.module"; diff --git a/apps/server/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts b/packages/core/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts similarity index 100% rename from apps/server/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts rename to packages/core/src/modules/cv-parser/__tests__/cv-parser.service.spec.ts diff --git a/apps/server/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts b/packages/core/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts similarity index 99% rename from apps/server/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts rename to packages/core/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts index a9a9704..54ae2e5 100644 --- a/apps/server/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts +++ b/packages/core/src/modules/cv-parser/__tests__/entity-resolver.service.spec.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "@cv/system"; +import { PrismaService } from "../../database"; import { Test, type TestingModule } from "@nestjs/testing"; import { EntityResolverService } from "../entity-resolver.service"; diff --git a/packages/core/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts b/packages/core/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts new file mode 100644 index 0000000..80f7ba3 --- /dev/null +++ b/packages/core/src/modules/cv-parser/__tests__/fixtures/mock-ai-responses.ts @@ -0,0 +1,184 @@ +import type { ParsedCVData } from "@cv/ai-parser"; + +/** + * Mock AI response for the plain text CV (John Smith) + */ +export const mockJohnSmithParsedCV: ParsedCVData = { + personalInfo: { + name: "John Smith", + headline: undefined, + introduction: + "Experienced software engineer with 8+ years of experience building scalable web applications and distributed systems.", + city: undefined, + country: undefined, + phone: undefined, + website: undefined, + linkedInUrl: undefined, + }, + jobExperiences: [ + { + companyName: "Acme Corporation", + roleName: "Senior Software Engineer", + levelName: "Senior", + startDate: "2020-01-01", + endDate: null, + description: + "Led development of microservices architecture serving 10M+ users. Mentored team of 5 junior developers.", + skills: ["TypeScript", "Node.js", "Kubernetes", "PostgreSQL", "Redis"], + }, + { + companyName: "TechStart Inc", + roleName: "Software Engineer", + levelName: "Mid-level", + startDate: "2016-03-01", + endDate: "2019-12-31", + description: + "Built REST APIs and GraphQL endpoints for e-commerce platform. Optimized database queries.", + skills: ["JavaScript", "Python", "MySQL", "Docker", "AWS"], + }, + { + companyName: "WebDev Agency", + roleName: "Junior Developer", + levelName: "Junior", + startDate: "2014-06-01", + endDate: "2016-02-28", + description: "Developed responsive websites for various clients.", + skills: ["PHP", "JavaScript", "HTML", "CSS"], + }, + ], + education: [ + { + institutionName: "Stanford University", + degree: "Bachelor of Science", + fieldOfStudy: "Computer Science", + startDate: "2010-09-01", + endDate: "2014-06-30", + description: "GPA: 3.8/4.0, Dean's List", + skills: ["Algorithms", "Data Structures", "C++"], + }, + ], + skills: [ + "TypeScript", + "JavaScript", + "Python", + "Node.js", + "React", + "GraphQL", + "PostgreSQL", + "MongoDB", + "Redis", + "Docker", + "Kubernetes", + "AWS", + "Git", + ], +}; + +/** + * Mock AI response for the markdown CV (Jane Doe) + */ +export const mockJaneDoeParsedCV: ParsedCVData = { + personalInfo: { + name: "Jane Doe", + headline: undefined, + introduction: + "Passionate full-stack developer with 6 years of experience in building modern web applications.", + city: undefined, + country: undefined, + phone: undefined, + website: undefined, + linkedInUrl: undefined, + }, + jobExperiences: [ + { + companyName: "Global Tech Solutions", + roleName: "Lead Developer", + levelName: "Lead", + startDate: "2021-04-01", + endDate: null, + description: + "Architected and led development of customer portal used by 50K+ enterprises.", + skills: [ + "React", + "TypeScript", + "NestJS", + "PostgreSQL", + "RabbitMQ", + "AWS", + ], + }, + { + companyName: "StartupXYZ", + roleName: "Software Engineer", + levelName: "Mid-level", + startDate: "2018-08-01", + endDate: "2021-03-31", + description: "Built real-time collaboration features using WebSockets.", + skills: ["React", "React Native", "Node.js", "MongoDB", "Firebase"], + }, + { + companyName: "Digital Creations Ltd", + roleName: "Junior Software Developer", + levelName: "Junior", + startDate: "2017-01-01", + endDate: "2018-07-31", + description: "Created RESTful APIs for content management system.", + skills: ["Express.js", "JavaScript", "MySQL", "Jest"], + }, + ], + education: [ + { + institutionName: "Columbia University", + degree: "Master of Science", + fieldOfStudy: "Software Engineering", + startDate: "2015-09-01", + endDate: "2017-05-31", + description: "Focus: Distributed Systems and Cloud Computing", + skills: [], + }, + { + institutionName: "NYU", + degree: "Bachelor of Arts", + fieldOfStudy: "Computer Science", + startDate: "2011-09-01", + endDate: "2015-05-31", + description: "Minor: Mathematics, Graduated Magna Cum Laude", + skills: [], + }, + ], + skills: [ + "TypeScript", + "JavaScript", + "Python", + "Go", + "React", + "React Native", + "Next.js", + "Vue.js", + "Node.js", + "NestJS", + "Express", + "GraphQL", + "PostgreSQL", + "MongoDB", + "Redis", + "AWS", + "Docker", + "Kubernetes", + "Terraform", + ], +}; + +/** + * Mock AI response wrapped in markdown code block (common LLM output format) + */ +export const createMockAIJsonResponse = (data: ParsedCVData): string => + `\`\`\`json +${JSON.stringify(data, null, 2)} +\`\`\``; + +/** + * Mock AI response as raw JSON (alternative LLM output format) + */ +export const createMockAIRawJsonResponse = (data: ParsedCVData): string => + JSON.stringify(data); diff --git a/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.md b/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.md new file mode 100644 index 0000000..1dc52d2 --- /dev/null +++ b/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.md @@ -0,0 +1,62 @@ +# Jane Doe +**Full Stack Developer** | New York, NY | jane.doe@email.com + +--- + +## Summary +Passionate full-stack developer with 6 years of experience in building modern web applications. Strong background in React, Node.js, and cloud technologies. Committed to writing maintainable, well-tested code. + +--- + +## Experience + +### Lead Developer +**Global Tech Solutions** | April 2021 - Present + +- Architected and led development of customer portal used by 50K+ enterprises +- Established coding standards and code review processes for team of 8 engineers +- Migrated legacy monolith to event-driven microservices architecture + +**Technologies:** React, TypeScript, NestJS, PostgreSQL, RabbitMQ, AWS + +### Software Engineer +**StartupXYZ** | August 2018 - March 2021 + +- Built real-time collaboration features using WebSockets +- Developed mobile-responsive UI components with React Native +- Integrated third-party payment and authentication services + +**Technologies:** React, React Native, Node.js, MongoDB, Firebase + +### Junior Software Developer +**Digital Creations Ltd** | January 2017 - July 2018 + +- Created RESTful APIs for content management system +- Wrote unit and integration tests achieving 85% code coverage +- Participated in agile ceremonies and sprint planning + +**Technologies:** Express.js, JavaScript, MySQL, Jest + +--- + +## Education + +### Master of Science in Software Engineering +**Columbia University** | September 2015 - May 2017 +- Focus: Distributed Systems and Cloud Computing +- Thesis: "Scalable Event-Driven Architectures for Real-Time Applications" + +### Bachelor of Arts in Computer Science +**NYU** | September 2011 - May 2015 +- Minor: Mathematics +- Graduated Magna Cum Laude + +--- + +## Skills + +**Languages:** TypeScript, JavaScript, Python, Go +**Frontend:** React, React Native, Next.js, Vue.js +**Backend:** Node.js, NestJS, Express, GraphQL +**Databases:** PostgreSQL, MongoDB, Redis +**Cloud & DevOps:** AWS, Docker, Kubernetes, Terraform, CI/CD diff --git a/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt b/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt new file mode 100644 index 0000000..646fbda --- /dev/null +++ b/packages/core/src/modules/cv-parser/__tests__/fixtures/sample-cv.txt @@ -0,0 +1,42 @@ +John Smith +Software Engineer | San Francisco, CA + +PROFESSIONAL SUMMARY +Experienced software engineer with 8+ years of experience building scalable web applications and distributed systems. Passionate about clean code, testing, and mentoring junior developers. + +WORK EXPERIENCE + +Senior Software Engineer +Acme Corporation +January 2020 - Present +- Led development of microservices architecture serving 10M+ users +- Mentored team of 5 junior developers +- Implemented CI/CD pipelines reducing deployment time by 60% +Skills: TypeScript, Node.js, Kubernetes, PostgreSQL, Redis + +Software Engineer +TechStart Inc +March 2016 - December 2019 +- Built REST APIs and GraphQL endpoints for e-commerce platform +- Optimized database queries improving response times by 40% +- Collaborated with product team on feature specifications +Skills: JavaScript, Python, MySQL, Docker, AWS + +Junior Developer +WebDev Agency +June 2014 - February 2016 +- Developed responsive websites for various clients +- Maintained legacy PHP applications +Skills: PHP, JavaScript, HTML, CSS + +EDUCATION + +Bachelor of Science in Computer Science +Stanford University +September 2010 - June 2014 +- GPA: 3.8/4.0 +- Dean's List +Skills: Algorithms, Data Structures, C++ + +SKILLS +TypeScript, JavaScript, Python, Node.js, React, GraphQL, PostgreSQL, MongoDB, Redis, Docker, Kubernetes, AWS, Git diff --git a/packages/core/src/modules/cv-parser/ai-provider-resolver.service.ts b/packages/core/src/modules/cv-parser/ai-provider-resolver.service.ts new file mode 100644 index 0000000..ba7b2ce --- /dev/null +++ b/packages/core/src/modules/cv-parser/ai-provider-resolver.service.ts @@ -0,0 +1,116 @@ +import { randomUUID } from "node:crypto"; +import { + AI_PROVIDER, + type AIProvider, + AnthropicProvider, + OpenAIProvider, +} from "@cv/ai-provider"; +import type { User } from "../auth"; +import { + BadRequestException, + ForbiddenException, + Inject, + Injectable, + Optional, +} from "@nestjs/common"; +import { AiCallLogService } from "../admin/ai-call-log.service"; +import { LoggingAIProvider } from "../admin/logging-ai-provider"; +import { UserAiSettingsService } from "../user-settings/user-ai-settings.service"; + +const DEFAULT_BASE_URLS: Record = { + anthropic: "https://api.anthropic.com", + openai: "https://api.openai.com", +}; + +/** + * Resolves the correct AIProvider for a given user based on their AI preference. + */ +@Injectable() +export class AIProviderResolverService { + constructor( + @Inject(AI_PROVIDER) + @Optional() + private readonly globalProvider: AIProvider | undefined, + private readonly userAiSettingsService: UserAiSettingsService, + private readonly aiCallLogService: AiCallLogService, + ) {} + + async resolveForUser(user: User): Promise { + try { + return await this.resolveProvider(user); + } catch (err) { + this.aiCallLogService.record({ + id: randomUUID(), + timestamp: new Date().toISOString(), + providerName: "resolution", + durationMs: 0, + status: "error", + error: err instanceof Error ? err.message : String(err), + userId: user.id, + source: "resolution", + }); + throw err; + } + } + + private async resolveProvider(user: User): Promise { + const settings = await this.userAiSettingsService.getOrCreateSettings(user); + + if (settings.aiPreference === "NO_AI") { + throw new ForbiddenException( + "AI is disabled. Enable it in profile settings.", + ); + } + + if (settings.aiPreference === "PLATFORM") { + if (!this.globalProvider) { + throw new BadRequestException( + "Platform AI is not available. Configure your own provider in profile settings.", + ); + } + return new LoggingAIProvider(this.globalProvider, this.aiCallLogService, { + userId: user.id, + source: "platform", + }); + } + + const resolved = + await this.userAiSettingsService.resolveActiveProvider(user); + + return this.instantiateProvider(resolved, user.id); + } + + private instantiateProvider( + config: { + providerType: string; + decryptedApiKey: string; + model: string | null; + baseUrl: string | null; + }, + userId: string, + ): AIProvider { + const baseUrl = + config.baseUrl || DEFAULT_BASE_URLS[config.providerType] || ""; + + const providerConfig = { + baseUrl, + apiKey: config.decryptedApiKey, + ...(config.model != null && { model: config.model }), + }; + + const provider = (() => { + if (config.providerType === "anthropic") + return new AnthropicProvider(providerConfig); + if (config.providerType === "openai") + return new OpenAIProvider(providerConfig); + throw new BadRequestException( + `Unsupported provider type: ${config.providerType}`, + ); + })(); + + return new LoggingAIProvider(provider, this.aiCallLogService, { + userId, + source: "byok", + }); + } +} diff --git a/packages/core/src/modules/cv-parser/cv-parser.module.ts b/packages/core/src/modules/cv-parser/cv-parser.module.ts new file mode 100644 index 0000000..e41009e --- /dev/null +++ b/packages/core/src/modules/cv-parser/cv-parser.module.ts @@ -0,0 +1,81 @@ +import { randomBytes } from "node:crypto"; +import { mkdirSync } from "node:fs"; +import { join } from "node:path"; +import { CVParserModule as CVParserCoreModule } from "@cv/ai-parser"; +import { AIModule } from "@cv/ai-provider"; +import { FileExtractionModule } from "@cv/file-upload"; +import { DatabaseModule } from "../database"; +import { Module } from "@nestjs/common"; +import { MulterModule } from "@nestjs/platform-express"; +import { diskStorage } from "multer"; +import { DataImportModule } from "../data-import/data-import.module"; +import { FileImportSource } from "../data-import/sources/file-import-source"; +import { EducationModule } from "../education/education.module"; +import { EmploymentModule } from "../job-experience/employment/employment.module"; +import { ProfileModule } from "../profile/profile.module"; +import { UserAiSettingsModule } from "../user-settings/user-ai-settings.module"; +import { AIProviderResolverService } from "./ai-provider-resolver.service"; +import { CVParserService } from "./cv-parser.service"; +import { EntityResolverService } from "./entity-resolver.service"; +import { FileUploadController } from "./file-upload.controller"; +import { ImportOnboardingStep } from "./onboarding/import.step"; + +@Module({ + imports: [ + CVParserCoreModule.forConfig(), + AIModule.forConfig(), + FileExtractionModule.forRoot(), + DatabaseModule, + UserAiSettingsModule, + ProfileModule, + EmploymentModule, + EducationModule, + DataImportModule, + MulterModule.register({ + storage: diskStorage({ + destination: (_req, _file, cb) => { + const tmpDir = join(process.cwd(), "tmp", "cv-uploads"); + mkdirSync(tmpDir, { recursive: true }); + cb(null, tmpDir); + }, + filename: (_req, file, cb) => { + const uniqueSuffix = `${Date.now()}-${randomBytes(8).toString("hex")}`; + const ext = file.originalname.split(".").pop() ?? ""; + cb(null, `${uniqueSuffix}.${ext}`); + }, + }), + limits: { + fileSize: 10 * 1024 * 1024, // 10MB + }, + fileFilter: (_req, file, cb) => { + const allowedMimes = [ + "application/pdf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "text/plain", + "text/markdown", + ]; + + if (allowedMimes.includes(file.mimetype)) { + cb(null, true); + } else { + cb( + new Error( + `File type ${file.mimetype} not allowed. Supported: PDF, DOCX, TXT, MD`, + ), + false, + ); + } + }, + }), + ], + providers: [ + EntityResolverService, + AIProviderResolverService, + CVParserService, + ImportOnboardingStep, + FileImportSource, + ], + controllers: [FileUploadController], + exports: [CVParserService], +}) +export class CVParserModule {} diff --git a/packages/core/src/modules/cv-parser/cv-parser.service.ts b/packages/core/src/modules/cv-parser/cv-parser.service.ts new file mode 100644 index 0000000..cf687a5 --- /dev/null +++ b/packages/core/src/modules/cv-parser/cv-parser.service.ts @@ -0,0 +1,178 @@ +import { CVParserService as CVParser, type ParsedCVData } from "@cv/ai-parser"; +import type { User } from "../auth"; +import { + TEXT_EXTRACTOR_REGISTRY, + type TextExtractorRegistry, + validateFile, +} from "@cv/file-upload"; +import { Inject, Injectable } from "@nestjs/common"; +import { AIProviderResolverService } from "./ai-provider-resolver.service"; +import { + EntityResolverService, + type ResolvedEducation, + type ResolvedJobExperience, +} from "./entity-resolver.service"; + +/** + * Parsed CV data with resolved entities + */ +export interface ParsedCVDataWithResolution { + jobExperiences: ResolvedJobExperience[]; + education: ResolvedEducation[]; +} + +@Injectable() +export class CVParserService { + constructor( + @Inject(TEXT_EXTRACTOR_REGISTRY) + private readonly textExtractorRegistry: TextExtractorRegistry, + private readonly entityResolver: EntityResolverService, + private readonly providerResolver: AIProviderResolverService, + ) {} + + /** Create a per-request CVParser using the user's resolved provider */ + private async createParser(user: User): Promise { + const provider = await this.providerResolver.resolveForUser(user); + return new CVParser(provider); + } + + async parseFile( + user: User, + buffer: Buffer, + mimeType: string, + originalName: string, + ): Promise { + const validation = validateFile({ + buffer, + mimeType, + originalName, + sizeBytes: buffer.length, + }); + + if (!validation.valid) { + throw new Error(`File validation failed: ${validation.error}`); + } + + const extraction = await this.textExtractorRegistry.extract( + buffer, + mimeType, + ); + + if (!extraction.success) { + throw new Error(`Text extraction failed: ${extraction.error}`); + } + + if (!extraction.text || extraction.text.trim().length === 0) { + throw new Error("Could not extract any text from the file"); + } + + const parser = await this.createParser(user); + return parser.parseCVText(extraction.text); + } + + async parseStory(user: User, storyText: string): Promise { + if (!storyText || storyText.trim().length === 0) { + throw new Error("Story text cannot be empty"); + } + + if (storyText.trim().length > 50000) { + throw new Error("Story text is too long (max 50,000 characters)"); + } + + const parser = await this.createParser(user); + return parser.parseCVText(storyText); + } + + /** + * Parse story text and resolve entities to existing database records + * Returns draft data with entity IDs where matches were found + */ + async parseStoryWithResolution( + user: User, + storyText: string, + ): Promise { + const parsed = await this.parseStory(user, storyText); + return this.resolveEntities(parsed); + } + + /** + * Parse file and resolve entities to existing database records + */ + async parseFileWithResolution( + user: User, + buffer: Buffer, + mimeType: string, + originalName: string, + ): Promise { + const parsed = await this.parseFile(user, buffer, mimeType, originalName); + return this.resolveEntities(parsed); + } + + /** + * Resolve entity names to existing database records + */ + private async resolveEntities( + parsed: ParsedCVData, + ): Promise { + const [jobExperiences, education] = await Promise.all([ + this.resolveJobExperiences(parsed.jobExperiences), + this.resolveEducation(parsed.education), + ]); + + return { jobExperiences, education }; + } + + /** + * Resolve job experience entities + */ + private async resolveJobExperiences( + jobs: ParsedCVData["jobExperiences"], + ): Promise { + return Promise.all( + jobs.map(async (job) => { + const [company, role, level, skills] = await Promise.all([ + this.entityResolver.resolveCompany(job.companyName), + this.entityResolver.resolveRole(job.roleName), + this.entityResolver.resolveLevel(job.levelName), + this.entityResolver.resolveSkills(job.skills ?? []), + ]); + + return { + company, + role, + level, + skills, + startDate: new Date(job.startDate), + endDate: job.endDate ? new Date(job.endDate) : null, + description: job.description ?? null, + }; + }), + ); + } + + /** + * Resolve education entities + */ + private async resolveEducation( + education: ParsedCVData["education"], + ): Promise { + return Promise.all( + education.map(async (edu) => { + const [institution, skills] = await Promise.all([ + this.entityResolver.resolveInstitution(edu.institutionName), + this.entityResolver.resolveSkills(edu.skills ?? []), + ]); + + return { + institution, + degree: edu.degree, + fieldOfStudy: edu.fieldOfStudy ?? null, + skills, + startDate: new Date(edu.startDate), + endDate: edu.endDate ? new Date(edu.endDate) : null, + description: edu.description ?? null, + }; + }), + ); + } +} diff --git a/packages/core/src/modules/cv-parser/entity-resolver.service.ts b/packages/core/src/modules/cv-parser/entity-resolver.service.ts new file mode 100644 index 0000000..a8a4539 --- /dev/null +++ b/packages/core/src/modules/cv-parser/entity-resolver.service.ts @@ -0,0 +1,120 @@ +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; + +/** + * Draft entity representation - ID is null if no match found + */ +export interface DraftEntity { + id: string | null; + name: string; +} + +/** + * Resolved job experience with draft entities + */ +export interface ResolvedJobExperience { + company: DraftEntity; + role: DraftEntity; + level: DraftEntity; + skills: DraftEntity[]; + startDate: Date; + endDate: Date | null; + description: string | null; +} + +/** + * Resolved education with draft entities + */ +export interface ResolvedEducation { + institution: DraftEntity; + degree: string; + fieldOfStudy: string | null; + skills: DraftEntity[]; + startDate: Date; + endDate: Date | null; + description: string | null; +} + +/** + * Service for resolving entity names to existing database entities + * Returns stubs with null IDs for unmatched names + */ +@Injectable() +export class EntityResolverService { + constructor(private readonly prisma: PrismaService) {} + + /** + * Resolve a company name to an existing entity + */ + async resolveCompany(name: string): Promise { + const match = await this.prisma.company.findFirst({ + where: { name: { equals: name, mode: "insensitive" } }, + }); + + return match ? { id: match.id, name: match.name } : { id: null, name }; + } + + /** + * Resolve a role name to an existing entity + */ + async resolveRole(name: string): Promise { + const match = await this.prisma.role.findFirst({ + where: { name: { equals: name, mode: "insensitive" } }, + }); + + return match ? { id: match.id, name: match.name } : { id: null, name }; + } + + /** + * Resolve a level name to an existing entity + * Falls back to "Mid-level" if not provided + */ + async resolveLevel(name?: string): Promise { + const searchName = name || "Mid-level"; + const match = await this.prisma.level.findFirst({ + where: { name: { equals: searchName, mode: "insensitive" } }, + }); + + return match + ? { id: match.id, name: match.name } + : { id: null, name: searchName }; + } + + /** + * Resolve a skill name to an existing entity + */ + async resolveSkill(name: string): Promise { + const trimmed = name.trim(); + if (!trimmed) { + return { id: null, name: "" }; + } + + const match = await this.prisma.skill.findFirst({ + where: { name: { equals: trimmed, mode: "insensitive" } }, + }); + + return match + ? { id: match.id, name: match.name } + : { id: null, name: trimmed }; + } + + /** + * Resolve an institution name to an existing entity + */ + async resolveInstitution(name: string): Promise { + const match = await this.prisma.institution.findFirst({ + where: { name: { equals: name, mode: "insensitive" } }, + }); + + return match ? { id: match.id, name: match.name } : { id: null, name }; + } + + /** + * Resolve multiple skills in parallel + */ + async resolveSkills(names: string[]): Promise { + const uniqueNames = [...new Set(names.filter((s) => s?.trim()))]; + + return Promise.all(uniqueNames.map((name) => this.resolveSkill(name))); + } +} diff --git a/packages/core/src/modules/cv-parser/file-upload.controller.ts b/packages/core/src/modules/cv-parser/file-upload.controller.ts new file mode 100644 index 0000000..a5e84f0 --- /dev/null +++ b/packages/core/src/modules/cv-parser/file-upload.controller.ts @@ -0,0 +1,97 @@ +import { createHash } from "node:crypto"; +import { readFile, unlink } from "node:fs/promises"; +import type { User as DomainUser } from "../auth"; +import { validateFile } from "@cv/file-upload"; +import { + BadRequestException, + Body, + Controller, + Logger, + Post, + Req, + UploadedFile, + UseInterceptors, +} from "@nestjs/common"; +import { FileInterceptor } from "@nestjs/platform-express"; +import { FileImportSource } from "../data-import/sources/file-import-source"; +import { ImportService } from "../data-import/import.service"; + +@Controller("api/cv-parser") +export class FileUploadController { + private readonly logger = new Logger(FileUploadController.name); + + constructor( + private readonly importService: ImportService, + private readonly fileImportSource: FileImportSource, + ) {} + + @Post("upload") + @UseInterceptors(FileInterceptor("file")) + async upload( + @Req() req: { user: DomainUser }, + @Body("profileId") profileId: string, + @UploadedFile() file?: Express.Multer.File, + ) { + if (!file) { + throw new BadRequestException("No file provided"); + } + + if (!profileId) { + throw new BadRequestException("profileId is required"); + } + + const filePath = file.path; + + try { + const buffer = file.buffer ?? (await readFile(filePath)); + + const validation = validateFile({ + buffer, + mimeType: file.mimetype, + originalName: file.originalname, + sizeBytes: file.size, + }); + + if (!validation.valid) { + throw new Error(`File validation failed: ${validation.error}`); + } + + const fingerprint = createHash("sha256").update(buffer).digest("hex"); + + const userFile = await this.importService.createImport( + req.user, + profileId, + this.fileImportSource, + { + fileName: file.originalname, + mimeType: file.mimetype, + sizeBytes: file.size, + fingerprint, + }, + { buffer, mimeType: file.mimetype }, + ); + + return { + userFileId: userFile.id, + status: userFile.status, + fileName: userFile.fileName, + sizeBytes: userFile.sizeBytes, + }; + } catch (error) { + this.logger.error(`File upload failed: ${error instanceof Error ? error.message : error}`, error instanceof Error ? error.stack : undefined); + + const message = + error instanceof Error ? error.message : "Failed to process file"; + + throw new BadRequestException(message); + } finally { + if (filePath) { + try { + await unlink(filePath); + } catch (err) { + this.logger.warn(`Failed to clean up temporary file ${filePath}: ${err instanceof Error ? err.message : err}`); + } + } + } + } +} diff --git a/packages/core/src/modules/cv-parser/onboarding/import.step.ts b/packages/core/src/modules/cv-parser/onboarding/import.step.ts new file mode 100644 index 0000000..051c1ba --- /dev/null +++ b/packages/core/src/modules/cv-parser/onboarding/import.step.ts @@ -0,0 +1,33 @@ +import type { User } from "../../auth"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { OnboardingStep } from "../../onboarding/onboarding-step.decorator"; +import { + type OnboardingStepDefinition, + OnboardingStepStatus, +} from "../../onboarding/onboarding-step.interface"; + +@Injectable() +@OnboardingStep() +export class ImportOnboardingStep implements OnboardingStepDefinition { + readonly name = "import"; + readonly order = 1; + readonly dependsOn = ["ai-preference"]; + + constructor(private readonly prisma: PrismaService) {} + + async computeStatus(user: User): Promise { + const [jobCount, eduCount] = await Promise.all([ + this.prisma.userJobExperience.count({ + where: { profile: { userId: user.id } }, + }), + this.prisma.education.count({ + where: { profile: { userId: user.id } }, + }), + ]); + + return jobCount > 0 || eduCount > 0 + ? OnboardingStepStatus.COMPLETE + : OnboardingStepStatus.NOT_STARTED; + } +} diff --git a/packages/core/src/modules/cv-parser/types/draft.types.ts b/packages/core/src/modules/cv-parser/types/draft.types.ts new file mode 100644 index 0000000..ac2d177 --- /dev/null +++ b/packages/core/src/modules/cv-parser/types/draft.types.ts @@ -0,0 +1,162 @@ +import { Field, ID, ObjectType } from "@nestjs/graphql"; +import type { + DraftEntity, + ResolvedEducation, + ResolvedJobExperience, +} from "../entity-resolver.service"; + +/** + * Draft entity - represents an entity that may or may not exist in the database + * If id is null, the entity needs to be created on save + */ +@ObjectType() +export class DraftEntityType { + @Field(() => ID, { nullable: true }) + id: string | null; + + @Field() + name: string; + + constructor(id: string | null, name: string) { + this.id = id; + this.name = name; + } + + static fromDraft(draft: DraftEntity): DraftEntityType { + return new DraftEntityType(draft.id, draft.name); + } +} + +/** + * Draft job experience - parsed from CV with resolved entities + */ +@ObjectType() +export class DraftJobExperienceType { + @Field(() => DraftEntityType) + company: DraftEntityType; + + @Field(() => DraftEntityType) + role: DraftEntityType; + + @Field(() => DraftEntityType) + level: DraftEntityType; + + @Field(() => [DraftEntityType]) + skills: DraftEntityType[]; + + @Field() + startDate: Date; + + @Field(() => Date, { nullable: true }) + endDate: Date | null; + + @Field(() => String, { nullable: true }) + description: string | null; + + constructor( + company: DraftEntityType, + role: DraftEntityType, + level: DraftEntityType, + skills: DraftEntityType[], + startDate: Date, + endDate: Date | null, + description: string | null, + ) { + this.company = company; + this.role = role; + this.level = level; + this.skills = skills; + this.startDate = startDate; + this.endDate = endDate; + this.description = description; + } + + static fromResolved(resolved: ResolvedJobExperience): DraftJobExperienceType { + return new DraftJobExperienceType( + DraftEntityType.fromDraft(resolved.company), + DraftEntityType.fromDraft(resolved.role), + DraftEntityType.fromDraft(resolved.level), + resolved.skills.map(DraftEntityType.fromDraft), + resolved.startDate, + resolved.endDate, + resolved.description, + ); + } +} + +/** + * Draft education - parsed from CV with resolved entities + */ +@ObjectType() +export class DraftEducationType { + @Field(() => DraftEntityType) + institution: DraftEntityType; + + @Field() + degree: string; + + @Field(() => String, { nullable: true }) + fieldOfStudy: string | null; + + @Field(() => [DraftEntityType]) + skills: DraftEntityType[]; + + @Field() + startDate: Date; + + @Field(() => Date, { nullable: true }) + endDate: Date | null; + + @Field(() => String, { nullable: true }) + description: string | null; + + constructor( + institution: DraftEntityType, + degree: string, + fieldOfStudy: string | null, + skills: DraftEntityType[], + startDate: Date, + endDate: Date | null, + description: string | null, + ) { + this.institution = institution; + this.degree = degree; + this.fieldOfStudy = fieldOfStudy; + this.skills = skills; + this.startDate = startDate; + this.endDate = endDate; + this.description = description; + } + + static fromResolved(resolved: ResolvedEducation): DraftEducationType { + return new DraftEducationType( + DraftEntityType.fromDraft(resolved.institution), + resolved.degree, + resolved.fieldOfStudy, + resolved.skills.map(DraftEntityType.fromDraft), + resolved.startDate, + resolved.endDate, + resolved.description, + ); + } +} + +/** + * Parsed CV data with resolved entities + */ +@ObjectType() +export class ParsedCVDataWithResolutionType { + @Field(() => [DraftJobExperienceType]) + jobExperiences: DraftJobExperienceType[]; + + @Field(() => [DraftEducationType]) + education: DraftEducationType[]; + + constructor( + jobExperiences: DraftJobExperienceType[], + education: DraftEducationType[], + ) { + this.jobExperiences = jobExperiences; + this.education = education; + } +} diff --git a/packages/core/src/modules/cv-template/cv-data-assembler.service.ts b/packages/core/src/modules/cv-template/cv-data-assembler.service.ts new file mode 100644 index 0000000..bb4ae4b --- /dev/null +++ b/packages/core/src/modules/cv-template/cv-data-assembler.service.ts @@ -0,0 +1,145 @@ +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import { ProfileService } from "../profile/profile.service"; + +export interface CVRenderContext { + cv: { title: string; introduction: string | null }; + profile: { + name: string; + headline: string | null; + phone: string | null; + locationId: string | null; + website: string | null; + linkedInUrl: string | null; + summary: string | null; + email: string | null; + }; + experience: Array<{ + company: string; + role: string; + level: string | null; + startDate: string; + endDate: string | null; + duration: string; + description: string | null; + skills: string[]; + }>; + education: Array<{ + institution: string; + degree: string; + fieldOfStudy: string | null; + startDate: string; + endDate: string | null; + duration: string; + description: string | null; + skills: string[]; + }>; + allSkills: string[]; + generatedAt: string; +} + +const MONTHS = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +] as const; + +const formatDate = (date: Date): string => + `${MONTHS[date.getMonth()]} ${date.getFullYear()}`; + +const computeDuration = (start: Date, end: Date | null): string => { + const endDate = end ?? new Date(); + const totalMonths = + (endDate.getFullYear() - start.getFullYear()) * 12 + + (endDate.getMonth() - start.getMonth()); + + const years = Math.floor(totalMonths / 12); + const months = totalMonths % 12; + + const parts: string[] = []; + if (years > 0) parts.push(`${years}y`); + if (months > 0) parts.push(`${months}m`); + + return parts.length === 0 ? "<1m" : parts.join(" "); +}; + +@Injectable() +export class CVDataAssemblerService { + constructor( + private readonly prisma: PrismaService, + private readonly profileService: ProfileService, + ) {} + + async assemble(cvId: string): Promise { + const cv = await this.prisma.cV.findUniqueOrThrow({ where: { id: cvId } }); + + const [profile, experiences, educations] = await Promise.all([ + this.profileService.findByIdOrFail(cv.profileId), + this.prisma.userJobExperience.findMany({ + where: { profileId: cv.profileId }, + orderBy: { startDate: "desc" }, + include: { company: true, role: true, level: true, skills: true }, + }), + this.prisma.education.findMany({ + where: { profileId: cv.profileId }, + orderBy: { startDate: "desc" }, + include: { institution: true, skills: true }, + }), + ]); + + const profileRecord = await this.prisma.profile.findUniqueOrThrow({ + where: { id: cv.profileId }, + select: { userId: true }, + }); + + const credential = await this.prisma.credentials.findFirst({ + where: { userId: profileRecord.userId }, + select: { email: true }, + }); + + const experienceItems = experiences.map((exp) => ({ + company: exp.company.name, + role: exp.role.name, + level: exp.level?.name ?? null, + startDate: formatDate(exp.startDate), + endDate: exp.endDate ? formatDate(exp.endDate) : null, + duration: computeDuration(exp.startDate, exp.endDate ?? null), + description: exp.description ?? null, + skills: exp.skills.map((s) => s.name), + })); + + const educationItems = educations.map((edu) => ({ + institution: edu.institution.name, + degree: edu.degree, + fieldOfStudy: edu.fieldOfStudy ?? null, + startDate: formatDate(edu.startDate), + endDate: edu.endDate ? formatDate(edu.endDate) : null, + duration: computeDuration(edu.startDate, edu.endDate ?? null), + description: edu.description ?? null, + skills: edu.skills.map((s) => s.name), + })); + + const allSkillNames = [ + ...experienceItems.flatMap((e) => e.skills), + ...educationItems.flatMap((e) => e.skills), + ]; + const allSkills = [...new Set(allSkillNames)].sort(); + + return { + cv: { title: cv.title, introduction: cv.introduction ?? null }, + profile: { + name: profile.fullName ?? profile.name, + headline: profile.headline ?? null, + phone: profile.phone ?? null, + locationId: profile.locationId ?? null, + website: profile.website ?? null, + linkedInUrl: profile.linkedInUrl ?? null, + summary: profile.summary ?? null, + email: credential?.email ?? null, + }, + experience: experienceItems, + education: educationItems, + allSkills, + generatedAt: new Date().toISOString(), + }; + } +} diff --git a/packages/core/src/modules/cv-template/cv-renderer.service.ts b/packages/core/src/modules/cv-template/cv-renderer.service.ts new file mode 100644 index 0000000..cf97581 --- /dev/null +++ b/packages/core/src/modules/cv-template/cv-renderer.service.ts @@ -0,0 +1,38 @@ +import { + type TemplateEngine, + HandlebarsEngine, + wrapInDocument, +} from "@cv/cv-renderer"; +import { Injectable } from "@nestjs/common"; +import { CVDataAssemblerService } from "./cv-data-assembler.service"; +import { CVTemplateService } from "./cv-template.service"; + +const engines: Record = { + handlebars: new HandlebarsEngine(), +}; + +@Injectable() +export class CVRendererService { + constructor( + private readonly cvTemplateService: CVTemplateService, + private readonly dataAssembler: CVDataAssemblerService, + ) {} + + async render(cvId: string, templateId: string): Promise { + const template = await this.cvTemplateService.findByIdOrFail(templateId); + + if (!template.body) { + throw new Error(`Template "${template.name}" has no body content`); + } + + const engine = engines[template.engine]; + if (!engine) { + throw new Error(`Unknown template engine: ${template.engine}`); + } + + const context = await this.dataAssembler.assemble(cvId); + const rendered = engine.render(template.body, context); + + return wrapInDocument(rendered, template.css ?? ""); + } +} diff --git a/packages/core/src/modules/cv-template/cv-template.entity.ts b/packages/core/src/modules/cv-template/cv-template.entity.ts new file mode 100644 index 0000000..00008a3 --- /dev/null +++ b/packages/core/src/modules/cv-template/cv-template.entity.ts @@ -0,0 +1,35 @@ +import { BaseEntity } from "../../shared"; + +export class CVTemplate extends BaseEntity { + name: string; + description?: string | null; + body?: string | null; + css?: string | null; + engine: string; + + constructor( + id: string, + name: string, + createdAt: Date, + updatedAt: Date, + description?: string | null, + body?: string | null, + css?: string | null, + engine: string = "handlebars", + ) { + super(id, createdAt, updatedAt); + this.name = name; + this.description = description ?? null; + this.body = body ?? null; + this.css = css ?? null; + this.engine = engine; + } +} + +export class RenderedCV { + html: string; + + constructor(html: string) { + this.html = html; + } +} diff --git a/packages/core/src/modules/cv-template/cv-template.mapper.ts b/packages/core/src/modules/cv-template/cv-template.mapper.ts new file mode 100644 index 0000000..632e6ea --- /dev/null +++ b/packages/core/src/modules/cv-template/cv-template.mapper.ts @@ -0,0 +1,19 @@ +import type { CVTemplate as PrismaCVTemplate } from "@prisma/client"; +import { CVTemplate } from "./cv-template.entity"; + +export const cvTemplateMapper = { + toDomain: (template: PrismaCVTemplate): CVTemplate => + new CVTemplate( + template.id, + template.name, + template.createdAt, + template.updatedAt, + template.description, + template.body, + template.css, + template.engine, + ), + + mapToDomain: (templates: PrismaCVTemplate[]): CVTemplate[] => + templates.map((template) => cvTemplateMapper.toDomain(template)), +}; diff --git a/packages/core/src/modules/cv-template/cv-template.module.ts b/packages/core/src/modules/cv-template/cv-template.module.ts new file mode 100644 index 0000000..4105276 --- /dev/null +++ b/packages/core/src/modules/cv-template/cv-template.module.ts @@ -0,0 +1,36 @@ +import { AuthorizationModule } from "../auth"; +import { BaseModule } from "../../shared"; +import { DatabaseModule } from "../database"; +import { Module, forwardRef } from "@nestjs/common"; +import { AuthenticationModule } from "../authentication/authentication.module"; +import { ProfileModule } from "../profile/profile.module"; +import { CVDataAssemblerService } from "./cv-data-assembler.service"; +import { CVRendererService } from "./cv-renderer.service"; +import { CVPolicy } from "./cv.policy"; +import { CVService } from "./cv.service"; +import { CVTemplatePolicy } from "./cv-template.policy"; +import { CVTemplateService } from "./cv-template.service"; +import { PdfDownloadController } from "./pdf-download.controller"; +import { CVTemplateSeedService } from "./seed/cv-template.seed"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + forwardRef(() => ProfileModule), + ], + controllers: [PdfDownloadController], + providers: [ + CVTemplateService, + CVService, + CVPolicy, + CVTemplatePolicy, + CVTemplateSeedService, + CVDataAssemblerService, + CVRendererService, + ], + exports: [CVTemplateService, CVService], +}) +export class CVTemplateModule {} diff --git a/packages/core/src/modules/cv-template/cv-template.policy.ts b/packages/core/src/modules/cv-template/cv-template.policy.ts new file mode 100644 index 0000000..0b4b392 --- /dev/null +++ b/packages/core/src/modules/cv-template/cv-template.policy.ts @@ -0,0 +1,7 @@ +import { Policy, PublicResourcePolicy } from "../auth"; +import { Injectable } from "@nestjs/common"; +import { CVTemplate } from "./cv-template.entity"; + +@Injectable() +@Policy(CVTemplate) +export class CVTemplatePolicy extends PublicResourcePolicy {} diff --git a/packages/core/src/modules/cv-template/cv-template.service.ts b/packages/core/src/modules/cv-template/cv-template.service.ts new file mode 100644 index 0000000..428f160 --- /dev/null +++ b/packages/core/src/modules/cv-template/cv-template.service.ts @@ -0,0 +1,36 @@ +import { notFound } from "../auth"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import { cvTemplateMapper } from "./cv-template.mapper"; + +type CVTemplateFilters = Record; + +@Injectable() +export class CVTemplateService { + constructor(private readonly prisma: PrismaService) {} + + async findMany(filters: CVTemplateFilters = {}) { + const templates = await this.prisma.cVTemplate.findMany({ + orderBy: { createdAt: "desc" }, + }); + + return cvTemplateMapper.mapToDomain(templates); + } + + async count(filters: CVTemplateFilters = {}) { + return this.prisma.cVTemplate.count(); + } + + async findById(id: string) { + const template = await this.prisma.cVTemplate.findUnique({ + where: { id }, + }); + + return template ? cvTemplateMapper.toDomain(template) : null; + } + + async findByIdOrFail(id: string) { + const template = await this.findById(id); + return template ?? notFound("CVTemplate", "id", id); + } +} diff --git a/packages/core/src/modules/cv-template/cv.entity.ts b/packages/core/src/modules/cv-template/cv.entity.ts new file mode 100644 index 0000000..79ad5cc --- /dev/null +++ b/packages/core/src/modules/cv-template/cv.entity.ts @@ -0,0 +1,25 @@ +import { BaseEntity } from "../../shared"; +import type { CVTemplate } from "./cv-template.entity"; + +export class CV extends BaseEntity { + profileId: string; + title: string; + introduction: string | null; + template: CVTemplate; + + constructor( + id: string, + profileId: string, + title: string, + createdAt: Date, + updatedAt: Date, + template: CVTemplate, + introduction?: string | null, + ) { + super(id, createdAt, updatedAt); + this.profileId = profileId; + this.title = title; + this.introduction = introduction ?? null; + this.template = template; + } +} diff --git a/packages/core/src/modules/cv-template/cv.mapper.ts b/packages/core/src/modules/cv-template/cv.mapper.ts new file mode 100644 index 0000000..ffd3529 --- /dev/null +++ b/packages/core/src/modules/cv-template/cv.mapper.ts @@ -0,0 +1,28 @@ +import type { + CV as PrismaCV, + CVTemplate as PrismaCVTemplate, +} from "@prisma/client"; +import { cvTemplateMapper } from "./cv-template.mapper"; +import { CV } from "./cv.entity"; + +export type PrismaCVWithTemplate = PrismaCV & { + template: PrismaCVTemplate; +}; + +export const cvMapper = { + toDomain: (cv: PrismaCVWithTemplate): CV => { + return new CV( + cv.id, + cv.profileId, + cv.title, + cv.createdAt, + cv.updatedAt, + cvTemplateMapper.toDomain(cv.template), + cv.introduction ?? null, + ); + }, + + mapToDomain: (cvs: PrismaCVWithTemplate[]): CV[] => { + return cvs.map((cv) => cvMapper.toDomain(cv)); + }, +}; diff --git a/packages/core/src/modules/cv-template/cv.policy.ts b/packages/core/src/modules/cv-template/cv.policy.ts new file mode 100644 index 0000000..55349d1 --- /dev/null +++ b/packages/core/src/modules/cv-template/cv.policy.ts @@ -0,0 +1,12 @@ +import { Policy, ProfileOwnedResourcePolicy } from "../auth"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import { CV } from "./cv.entity"; + +@Injectable() +@Policy(CV) +export class CVPolicy extends ProfileOwnedResourcePolicy { + constructor(prisma: PrismaService) { + super(prisma); + } +} diff --git a/packages/core/src/modules/cv-template/cv.service.ts b/packages/core/src/modules/cv-template/cv.service.ts new file mode 100644 index 0000000..d1a8b20 --- /dev/null +++ b/packages/core/src/modules/cv-template/cv.service.ts @@ -0,0 +1,134 @@ +import { notFound } from "../auth"; +import type { PaginationOptions, PaginationResult } from "../../shared"; +import { type EntityService, PaginationService } from "../../shared"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; +import { cvMapper } from "./cv.mapper"; +import { CV } from "./cv.entity"; + +type CVFilters = { + profileId?: string | string[]; + id?: string | string[]; +}; + +type CVWhereInput = { + profileId?: { in: string[] } | string; + id?: { in: string[] } | string; +}; + +@Injectable() +export class CVService implements EntityService { + private readonly cvInclude = { + template: true, + } satisfies Prisma.CVInclude; + + constructor( + private readonly prisma: PrismaService, + private readonly paginationService: PaginationService, + ) {} + + async findById(id: string): Promise { + const cv = await this.prisma.cV.findUnique({ + where: { id }, + include: this.cvInclude, + }); + + return cv ? cvMapper.toDomain(cv) : null; + } + + async findByIdOrFail(id: string): Promise { + const cv = await this.findById(id); + return cv ?? notFound("CV", "id", id); + } + + private buildWhere(filters: CVFilters): CVWhereInput { + const where: CVWhereInput = {}; + + if (filters.profileId !== undefined) { + where.profileId = Array.isArray(filters.profileId) + ? { in: filters.profileId } + : filters.profileId; + } + + if (filters.id !== undefined) { + where.id = Array.isArray(filters.id) ? { in: filters.id } : filters.id; + } + + return where; + } + + async findMany(filters: CVFilters): Promise { + const cvs = await this.prisma.cV.findMany({ + where: this.buildWhere(filters), + orderBy: { updatedAt: "desc" }, + include: this.cvInclude, + }); + + return cvMapper.mapToDomain(cvs); + } + + async findManyForProfile( + profileId: string, + options: PaginationOptions = {}, + ): Promise> { + const where = { profileId }; + const orderBy = { updatedAt: "desc" as const }; + const queryOptions = this.paginationService.buildQueryOptions( + where, + orderBy, + options, + ); + + const [items, totalCount] = await Promise.all([ + this.prisma.cV.findMany({ + ...queryOptions, + include: this.cvInclude, + }), + this.prisma.cV.count({ where }), + ]); + + return this.paginationService.buildPaginationResult( + cvMapper.mapToDomain(items), + totalCount, + options, + ); + } + + async count(filters: CVFilters): Promise { + return this.prisma.cV.count({ where: this.buildWhere(filters) }); + } + + async save(entity: CV): Promise { + const shared = { + title: entity.title, + introduction: entity.introduction, + }; + + const cv = await this.prisma.cV.upsert({ + where: { id: entity.id }, + create: { + id: entity.id, + ...shared, + profile: { connect: { id: entity.profileId } }, + template: { connect: { id: entity.template.id } }, + }, + update: { + ...shared, + template: { connect: { id: entity.template.id } }, + }, + include: this.cvInclude, + }); + const domain = cvMapper.toDomain(cv); + if (!domain) { + throw new Error("Failed to map CV to domain"); + } + return domain; + } + + async destroy(entity: CV): Promise { + await this.prisma.cV.delete({ + where: { id: entity.id }, + }); + } +} diff --git a/packages/core/src/modules/cv-template/pdf-download.controller.ts b/packages/core/src/modules/cv-template/pdf-download.controller.ts new file mode 100644 index 0000000..5ca2397 --- /dev/null +++ b/packages/core/src/modules/cv-template/pdf-download.controller.ts @@ -0,0 +1,50 @@ +import type { User as DomainUser } from "../auth"; +import { AuthorizationService } from "../auth"; +import { FILE_STORAGE, type FileStorage } from "@cv/file-storage"; +import { + Controller, + Get, + Inject, + NotFoundException, + Param, + Req, + Res, +} from "@nestjs/common"; +import type { Response } from "express"; +import { CVService } from "./cv.service"; +import { CV } from "./cv.entity"; + +@Controller("api/cv") +export class PdfDownloadController { + constructor( + private readonly cvService: CVService, + private readonly authorizationService: AuthorizationService, + @Inject(FILE_STORAGE) private readonly storage: FileStorage, + ) {} + + @Get(":id/pdf") + async downloadPdf( + @Req() req: { user: DomainUser }, + @Param("id") id: string, + @Res() res: Response, + ): Promise { + const cv = await this.cvService.findByIdOrFail(id); + await this.authorizationService.canView(req.user, cv, CV); + + const key = `${id}.pdf`; + + if (!(await this.storage.exists(key))) { + throw new NotFoundException("PDF not yet available"); + } + + const pdf = await this.storage.read(key); + const safeTitle = cv.title.replace(/[^a-zA-Z0-9_\- ]/g, ""); + + res.set({ + "Content-Type": "application/pdf", + "Content-Disposition": `attachment; filename="${safeTitle}.pdf"`, + "Content-Length": pdf.length.toString(), + }); + res.send(pdf); + } +} diff --git a/packages/core/src/modules/cv-template/seed/cv-template.seed.ts b/packages/core/src/modules/cv-template/seed/cv-template.seed.ts new file mode 100644 index 0000000..0a46c83 --- /dev/null +++ b/packages/core/src/modules/cv-template/seed/cv-template.seed.ts @@ -0,0 +1,76 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { PrismaService } from "../../database"; +import { Injectable, Logger } from "@nestjs/common"; +import { Seeder } from "../../database/seed/seed.service"; +import { Seeder as SeederDecorator } from "../../database/seed/seeder.decorator"; + +const TEMPLATES_DIR = join(__dirname, "templates"); + +const readTemplate = (name: string) => ({ + body: readFileSync(join(TEMPLATES_DIR, `${name}.hbs`), "utf-8"), + css: readFileSync(join(TEMPLATES_DIR, `${name}.css`), "utf-8"), +}); + +interface TemplateData { + name: string; + description: string; + body: string; + css: string; + engine: string; +} + +const templates: TemplateData[] = [ + { + name: "Modern Professional", + description: "A clean, modern template perfect for tech professionals", + ...readTemplate("modern-professional"), + engine: "handlebars", + }, + { + name: "Classic Executive", + description: "A traditional template suitable for executive positions", + ...readTemplate("classic-executive"), + engine: "handlebars", + }, + { + name: "Creative Portfolio", + description: "A creative template for designers and artists", + ...readTemplate("creative-portfolio"), + engine: "handlebars", + }, +]; + +@Injectable() +@SeederDecorator("CV Templates") +export class CVTemplateSeedService implements Seeder { + private readonly logger = new Logger(CVTemplateSeedService.name); + + constructor(readonly _prisma: PrismaService) {} + + async seed(prisma: PrismaService): Promise { + this.logger.log("Seeding CV templates..."); + + await Promise.all( + templates.map(async (template) => { + const existing = await prisma["cVTemplate"].findFirst({ + where: { name: template.name }, + }); + + if (existing) { + await prisma["cVTemplate"].update({ + where: { id: existing.id }, + data: { + description: template.description, + body: template.body, + css: template.css, + engine: template.engine, + }, + }); + } else { + await prisma["cVTemplate"].create({ data: template }); + } + }), + ); + } +} diff --git a/packages/core/src/modules/cv-template/seed/templates/classic-executive.css b/packages/core/src/modules/cv-template/seed/templates/classic-executive.css new file mode 100644 index 0000000..318f298 --- /dev/null +++ b/packages/core/src/modules/cv-template/seed/templates/classic-executive.css @@ -0,0 +1,24 @@ +body { + font-family: Georgia, 'Times New Roman', serif; + color: #222; + line-height: 1.5; + font-size: 14px; + padding: 2rem; +} +.cv { max-width: 780px; margin: 0 auto; } +.header { text-align: center; border-bottom: 2px solid #222; padding-bottom: 1rem; margin-bottom: 1.5rem; } +.header h1 { font-size: 2rem; font-weight: 400; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 0.25rem; } +.headline { font-style: italic; color: #555; font-size: 1rem; margin-bottom: 0.5rem; } +.contact-row { display: flex; justify-content: center; flex-wrap: wrap; gap: 1.2rem; font-size: 0.85rem; color: #555; } +.section { margin-bottom: 1.5rem; } +.section h2 { font-size: 1.1rem; text-transform: uppercase; letter-spacing: 0.08em; border-bottom: 1px solid #999; padding-bottom: 0.2rem; margin-bottom: 0.75rem; color: #333; } +.entry { margin-bottom: 1rem; } +.entry-header { display: flex; justify-content: space-between; align-items: baseline; } +.org { color: #555; font-size: 0.9rem; margin-bottom: 0.25rem; } +.dates { font-size: 0.85rem; color: #555; white-space: nowrap; } +.description { margin-top: 0.3rem; color: #333; white-space: pre-line; } +.skill-line { font-size: 0.9rem; color: #444; margin-top: 0.3rem; } +.intro { font-style: italic; color: #444; } +@media print { + body { padding: 0; font-size: 11pt; } +} diff --git a/packages/core/src/modules/cv-template/seed/templates/classic-executive.hbs b/packages/core/src/modules/cv-template/seed/templates/classic-executive.hbs new file mode 100644 index 0000000..0b5e510 --- /dev/null +++ b/packages/core/src/modules/cv-template/seed/templates/classic-executive.hbs @@ -0,0 +1,66 @@ +
+
+

{{profile.name}}

+ {{#if profile.headline}}

{{profile.headline}}

{{/if}} +
+ {{#if profile.email}}{{profile.email}}{{/if}} + {{#if profile.phone}}{{profile.phone}}{{/if}} + {{#if profile.city}}{{profile.city}}{{#if profile.country}}, {{profile.country}}{{/if}}{{/if}} + {{#if profile.website}}{{profile.website}}{{/if}} + {{#if profile.linkedInUrl}}{{profile.linkedInUrl}}{{/if}} +
+
+ + {{#if profile.summary}} +
+

Professional Summary

+

{{profile.summary}}

+
+ {{/if}} + + {{#if cv.introduction}} +
+

{{cv.introduction}}

+
+ {{/if}} + + {{#if (hasItems experience)}} +
+

Professional Experience

+ {{#each experience}} +
+
+ {{this.role}} + {{this.startDate}} – {{#if this.endDate}}{{this.endDate}}{{else}}Present{{/if}} ({{this.duration}}) +
+

{{this.company}}{{#if this.level}}, {{this.level}}{{/if}}

+ {{#if this.description}}

{{this.description}}

{{/if}} + {{#if (hasItems this.skills)}}

Key skills: {{join this.skills ", "}}

{{/if}} +
+ {{/each}} +
+ {{/if}} + + {{#if (hasItems education)}} +
+

Education

+ {{#each education}} +
+
+ {{this.degree}}{{#if this.fieldOfStudy}}, {{this.fieldOfStudy}}{{/if}} + {{this.startDate}} – {{#if this.endDate}}{{this.endDate}}{{else}}Present{{/if}} +
+

{{this.institution}}

+ {{#if this.description}}

{{this.description}}

{{/if}} +
+ {{/each}} +
+ {{/if}} + + {{#if (hasItems allSkills)}} +
+

Core Competencies

+

{{join allSkills " • "}}

+
+ {{/if}} +
diff --git a/packages/core/src/modules/cv-template/seed/templates/creative-portfolio.css b/packages/core/src/modules/cv-template/seed/templates/creative-portfolio.css new file mode 100644 index 0000000..1890c23 --- /dev/null +++ b/packages/core/src/modules/cv-template/seed/templates/creative-portfolio.css @@ -0,0 +1,35 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + color: #2d3748; + line-height: 1.6; + font-size: 14px; + margin: 0; + padding: 0; +} +.cv { display: flex; min-height: 100vh; } +.sidebar { width: 280px; background: #1a1a2e; color: #e2e8f0; padding: 2rem 1.5rem; flex-shrink: 0; } +.sidebar-header h1 { font-size: 1.5rem; font-weight: 700; color: #fff; margin-bottom: 0.25rem; } +.headline { color: #a78bfa; font-size: 0.95rem; } +.sidebar-section { margin-top: 1.5rem; } +.sidebar-section h3 { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.1em; color: #a78bfa; margin-bottom: 0.5rem; } +.sidebar-section p { font-size: 0.85rem; margin-bottom: 0.25rem; } +.sidebar-section a { color: #c4b5fd; text-decoration: none; } +.skill-tags { display: flex; flex-wrap: wrap; gap: 0.3rem; } +.tag { background: rgba(167,139,250,0.2); color: #c4b5fd; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.75rem; } +.main { flex: 1; padding: 2rem; } +.section { margin-bottom: 1.5rem; } +.section h2 { font-size: 1.15rem; color: #1a1a2e; border-bottom: 2px solid #a78bfa; padding-bottom: 0.2rem; margin-bottom: 0.75rem; } +.entry { margin-bottom: 1rem; } +.entry-header { display: flex; justify-content: space-between; align-items: baseline; flex-wrap: wrap; } +.entry-header h3 { font-size: 1rem; font-weight: 600; } +.org { color: #718096; font-size: 0.9rem; } +.dates { font-size: 0.85rem; color: #718096; white-space: nowrap; } +.description { margin-top: 0.3rem; color: #4a5568; white-space: pre-line; } +.intro { color: #718096; font-style: italic; } +.inline-skills { display: flex; flex-wrap: wrap; gap: 0.3rem; margin-top: 0.4rem; } +.inline-tag { background: #ede9fe; color: #6d28d9; padding: 0.1rem 0.5rem; border-radius: 4px; font-size: 0.75rem; } +@media print { + .cv { display: flex; } + .sidebar { background: #1a1a2e !important; color: #e2e8f0 !important; } + .tag { border: 1px solid #a78bfa; } +} diff --git a/packages/core/src/modules/cv-template/seed/templates/creative-portfolio.hbs b/packages/core/src/modules/cv-template/seed/templates/creative-portfolio.hbs new file mode 100644 index 0000000..ea876f5 --- /dev/null +++ b/packages/core/src/modules/cv-template/seed/templates/creative-portfolio.hbs @@ -0,0 +1,72 @@ +
+ + +
+ {{#if profile.summary}} +
+

Profile

+

{{profile.summary}}

+
+ {{/if}} + + {{#if cv.introduction}} +
+

{{cv.introduction}}

+
+ {{/if}} + + {{#if (hasItems experience)}} +
+

Experience

+ {{#each experience}} +
+
+

{{this.role}}

+ {{this.startDate}} – {{#if this.endDate}}{{this.endDate}}{{else}}Present{{/if}} · {{this.duration}} +
+

{{this.company}}{{#if this.level}} · {{this.level}}{{/if}}

+ {{#if this.description}}

{{this.description}}

{{/if}} + {{#if (hasItems this.skills)}}
{{#each this.skills}}{{this}}{{/each}}
{{/if}} +
+ {{/each}} +
+ {{/if}} + + {{#if (hasItems education)}} +
+

Education

+ {{#each education}} +
+
+

{{this.degree}}

+ {{this.startDate}} – {{#if this.endDate}}{{this.endDate}}{{else}}Present{{/if}} +
+

{{this.institution}}{{#if this.fieldOfStudy}} · {{this.fieldOfStudy}}{{/if}}

+ {{#if this.description}}

{{this.description}}

{{/if}} +
+ {{/each}} +
+ {{/if}} +
+
diff --git a/packages/core/src/modules/cv-template/seed/templates/modern-professional.css b/packages/core/src/modules/cv-template/seed/templates/modern-professional.css new file mode 100644 index 0000000..0840be5 --- /dev/null +++ b/packages/core/src/modules/cv-template/seed/templates/modern-professional.css @@ -0,0 +1,29 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + color: #1a1a2e; + line-height: 1.6; + font-size: 14px; + padding: 2rem; +} +.cv { max-width: 800px; margin: 0 auto; } +.header { border-bottom: 2px solid #2563eb; padding-bottom: 1rem; margin-bottom: 1.5rem; } +.header h1 { font-size: 2rem; font-weight: 700; margin-bottom: 0.25rem; } +.headline { color: #2563eb; font-size: 1.1rem; margin-bottom: 0.5rem; } +.contact-row { display: flex; flex-wrap: wrap; gap: 1rem; font-size: 0.85rem; color: #555; } +.contact-row a { color: #2563eb; text-decoration: none; } +.section { margin-bottom: 1.5rem; } +.section h2 { font-size: 1.2rem; text-transform: uppercase; letter-spacing: 0.05em; color: #2563eb; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.25rem; margin-bottom: 0.75rem; } +.entry { margin-bottom: 1rem; } +.entry-header { display: flex; justify-content: space-between; align-items: flex-start; } +.entry-header h3 { font-size: 1rem; font-weight: 600; } +.company { color: #555; font-size: 0.9rem; } +.dates { text-align: right; font-size: 0.85rem; color: #555; white-space: nowrap; } +.duration { display: block; font-size: 0.8rem; color: #888; } +.description { margin-top: 0.4rem; color: #333; white-space: pre-line; } +.skills { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; } +.tag { background: #eff6ff; color: #2563eb; padding: 0.15rem 0.6rem; border-radius: 9999px; font-size: 0.8rem; } +@media print { + body { padding: 0; font-size: 11pt; } + .header { border-color: #2563eb; } + .tag { border: 1px solid #2563eb; background: transparent; } +} diff --git a/packages/core/src/modules/cv-template/seed/templates/modern-professional.hbs b/packages/core/src/modules/cv-template/seed/templates/modern-professional.hbs new file mode 100644 index 0000000..e95297f --- /dev/null +++ b/packages/core/src/modules/cv-template/seed/templates/modern-professional.hbs @@ -0,0 +1,78 @@ +
+
+

{{profile.name}}

+ {{#if profile.headline}}

{{profile.headline}}

{{/if}} +
+ {{#if profile.email}}{{profile.email}}{{/if}} + {{#if profile.phone}}{{profile.phone}}{{/if}} + {{#if profile.city}}{{profile.city}}{{#if profile.country}}, {{profile.country}}{{/if}}{{/if}} + {{#if profile.website}}{{profile.website}}{{/if}} + {{#if profile.linkedInUrl}}LinkedIn{{/if}} +
+
+ + {{#if profile.summary}} +
+

Profile

+

{{profile.summary}}

+
+ {{/if}} + + {{#if cv.introduction}} +
+

About This CV

+

{{cv.introduction}}

+
+ {{/if}} + + {{#if (hasItems experience)}} +
+

Experience

+ {{#each experience}} +
+
+
+

{{this.role}}

+

{{this.company}}{{#if this.level}} · {{this.level}}{{/if}}

+
+
+ {{this.startDate}} – {{#if this.endDate}}{{this.endDate}}{{else}}Present{{/if}} + {{this.duration}} +
+
+ {{#if this.description}}

{{this.description}}

{{/if}} + {{#if (hasItems this.skills)}}
{{#each this.skills}}{{this}}{{/each}}
{{/if}} +
+ {{/each}} +
+ {{/if}} + + {{#if (hasItems education)}} +
+

Education

+ {{#each education}} +
+
+
+

{{this.degree}}

+

{{this.institution}}{{#if this.fieldOfStudy}} · {{this.fieldOfStudy}}{{/if}}

+
+
+ {{this.startDate}} – {{#if this.endDate}}{{this.endDate}}{{else}}Present{{/if}} + {{this.duration}} +
+
+ {{#if this.description}}

{{this.description}}

{{/if}} + {{#if (hasItems this.skills)}}
{{#each this.skills}}{{this}}{{/each}}
{{/if}} +
+ {{/each}} +
+ {{/if}} + + {{#if (hasItems allSkills)}} +
+

Skills

+
{{#each allSkills}}{{this}}{{/each}}
+
+ {{/if}} +
diff --git a/packages/core/src/modules/data-import/data-import-source.interface.ts b/packages/core/src/modules/data-import/data-import-source.interface.ts new file mode 100644 index 0000000..a7c4442 --- /dev/null +++ b/packages/core/src/modules/data-import/data-import-source.interface.ts @@ -0,0 +1,11 @@ +import type { ParsedCVData } from "@cv/ai-parser"; +import type { User } from "../auth"; + +export interface DataImportSource { + readonly name: string; + execute( + user: User, + params: Record, + onStatus: (message: string) => Promise, + ): Promise; +} diff --git a/packages/core/src/modules/data-import/data-import.module.ts b/packages/core/src/modules/data-import/data-import.module.ts new file mode 100644 index 0000000..afbae43 --- /dev/null +++ b/packages/core/src/modules/data-import/data-import.module.ts @@ -0,0 +1,26 @@ +import { AuthorizationModule } from "../auth"; +import { DatabaseModule } from "../database"; +import { Module } from "@nestjs/common"; +import { EntityResolverService } from "../cv-parser/entity-resolver.service"; +import { ProfileModule } from "../profile/profile.module"; +import { ImportService } from "./import.service"; +import { ImportJobMapper } from "./import-job.mapper"; +import { ImportJobPolicy } from "./import-job.policy"; +import { ImportJobListener } from "./listeners/import-job.listener"; +import { UserFileMapper } from "./user-file.mapper"; +import { UserFilePolicy } from "./user-file.policy"; + +@Module({ + imports: [DatabaseModule, AuthorizationModule, ProfileModule], + providers: [ + ImportService, + EntityResolverService, + UserFileMapper, + UserFilePolicy, + ImportJobMapper, + ImportJobPolicy, + ImportJobListener, + ], + exports: [ImportService, UserFileMapper, ImportJobMapper], +}) +export class DataImportModule {} diff --git a/packages/core/src/modules/data-import/events/user-file-created.event.ts b/packages/core/src/modules/data-import/events/user-file-created.event.ts new file mode 100644 index 0000000..68195f4 --- /dev/null +++ b/packages/core/src/modules/data-import/events/user-file-created.event.ts @@ -0,0 +1,12 @@ +import { DomainEvent } from "../../events"; +import type { User } from "../../auth"; +import type { DataImportSource } from "../data-import-source.interface"; + +interface UserFileCreatedPayload { + userFileId: string; + user: User; + source: DataImportSource; + params: Record; +} + +export class UserFileCreatedEvent extends DomainEvent {} diff --git a/packages/core/src/modules/data-import/import-job.entity.ts b/packages/core/src/modules/data-import/import-job.entity.ts new file mode 100644 index 0000000..de6b1bf --- /dev/null +++ b/packages/core/src/modules/data-import/import-job.entity.ts @@ -0,0 +1,18 @@ +import { BaseEntity } from "../../shared"; + +export class ImportJob extends BaseEntity { + constructor( + id: string, + public readonly userFileId: string, + public readonly source: string, + public readonly status: string, + public readonly statusMessage: string, + public readonly error: string | null, + public readonly startedAt: Date | null, + public readonly completedAt: Date | null, + createdAt: Date, + updatedAt: Date, + ) { + super(id, createdAt, updatedAt); + } +} diff --git a/packages/core/src/modules/data-import/import-job.mapper.ts b/packages/core/src/modules/data-import/import-job.mapper.ts new file mode 100644 index 0000000..d07c68a --- /dev/null +++ b/packages/core/src/modules/data-import/import-job.mapper.ts @@ -0,0 +1,36 @@ +import type { BaseMapper } from "../../shared"; +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; + +type PrismaImportJob = Prisma.ImportJobGetPayload; + +import { ImportJob } from "./import-job.entity"; + +@Injectable() +export class ImportJobMapper implements BaseMapper { + toDomain(prisma: null): null; + toDomain(prisma: PrismaImportJob): ImportJob; + toDomain(prisma: PrismaImportJob | null): ImportJob | null; + toDomain(prisma: PrismaImportJob | null): ImportJob | null { + if (!prisma) return null; + + return new ImportJob( + prisma.id, + prisma.userFileId, + prisma.source, + prisma.status, + prisma.statusMessage, + prisma.error, + prisma.startedAt, + prisma.completedAt, + prisma.createdAt, + prisma.updatedAt, + ); + } + + mapToDomain(items: PrismaImportJob[]): ImportJob[] { + return items + .map((item) => this.toDomain(item)) + .filter((item): item is ImportJob => item !== null); + } +} diff --git a/packages/core/src/modules/data-import/import-job.policy.ts b/packages/core/src/modules/data-import/import-job.policy.ts new file mode 100644 index 0000000..f586a2c --- /dev/null +++ b/packages/core/src/modules/data-import/import-job.policy.ts @@ -0,0 +1,28 @@ +import type { IPolicy, User } from "../auth"; +import { Policy } from "../auth"; +import { Injectable } from "@nestjs/common"; +import { ImportJob } from "./import-job.entity"; + +/** + * ImportJob is an internal/admin-only entity. + * All user-facing operations go through UserFile instead. + */ +@Injectable() +@Policy(ImportJob) +export class ImportJobPolicy implements IPolicy { + view(_user: User, _resource: ImportJob): boolean { + return false; + } + + create(_user: User, _resource?: Partial): boolean { + return false; + } + + update(_user: User, _resource: ImportJob): boolean { + return false; + } + + delete(_user: User, _resource: ImportJob): boolean { + return false; + } +} diff --git a/packages/core/src/modules/data-import/import.service.ts b/packages/core/src/modules/data-import/import.service.ts new file mode 100644 index 0000000..480b0f7 --- /dev/null +++ b/packages/core/src/modules/data-import/import.service.ts @@ -0,0 +1,85 @@ +import type { User } from "../auth"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import { EventService } from "../events"; +import type { DataImportSource } from "./data-import-source.interface"; +import { UserFileCreatedEvent } from "./events/user-file-created.event"; +import { UserFile } from "./user-file.entity"; +import { UserFileMapper } from "./user-file.mapper"; + +@Injectable() +export class ImportService { + constructor( + private readonly prisma: PrismaService, + private readonly userFileMapper: UserFileMapper, + private readonly eventService: EventService, + ) {} + + /** + * Find an existing completed UserFile with the same fingerprint for this user. + */ + async findDuplicateForUser( + userId: string, + fingerprint: string, + ): Promise { + const record = await this.prisma.userFile.findFirst({ + where: { + fingerprint, + profile: { userId }, + status: "completed", + }, + orderBy: { createdAt: "desc" }, + }); + return this.userFileMapper.toDomain(record); + } + + /** + * Create a UserFile and emit an event for async processing. + */ + async createImport( + user: User, + profileId: string, + source: DataImportSource, + file: { fileName: string; mimeType: string; sizeBytes: number; fingerprint?: string }, + params: Record, + ): Promise { + const record = await this.prisma.userFile.create({ + data: { + profile: { connect: { id: profileId } }, + fingerprint: file.fingerprint ?? null, + fileName: file.fileName, + mimeType: file.mimeType, + sizeBytes: file.sizeBytes, + source: source.name, + status: "pending", + statusMessage: "Queued for processing", + }, + }); + + this.eventService.emit( + new UserFileCreatedEvent({ userFileId: record.id, user, source, params }), + ); + + return this.userFileMapper.toDomain(record) as UserFile; + } + + /** + * Delete a UserFile and its cascading ImportJobs. + */ + async deleteUserFile(id: string): Promise { + await this.prisma.userFile.delete({ where: { id } }); + } + + async findUserFileById(id: string): Promise { + const record = await this.prisma.userFile.findUnique({ where: { id } }); + return this.userFileMapper.toDomain(record); + } + + async findUserFilesForUser(userId: string): Promise { + const records = await this.prisma.userFile.findMany({ + where: { profile: { userId } }, + orderBy: { createdAt: "desc" }, + }); + return this.userFileMapper.mapToDomain(records); + } +} diff --git a/packages/core/src/modules/data-import/listeners/import-job.listener.ts b/packages/core/src/modules/data-import/listeners/import-job.listener.ts new file mode 100644 index 0000000..4e0c3fb --- /dev/null +++ b/packages/core/src/modules/data-import/listeners/import-job.listener.ts @@ -0,0 +1,268 @@ +import type { ParsedCVData } from "@cv/ai-parser"; +import { PrismaService } from "../../database"; +import { Injectable, Logger } from "@nestjs/common"; +import { OnEvent } from "@nestjs/event-emitter"; +import { + EntityResolverService, + type ResolvedEducation, + type ResolvedJobExperience, +} from "../../cv-parser/entity-resolver.service"; +import { ProfileService } from "../../profile/profile.service"; +import { UserFileCreatedEvent } from "../events/user-file-created.event"; + +interface ResolvedResult { + personalInfo?: { + name?: string | undefined; + headline?: string | undefined; + introduction?: string | undefined; + city?: string | undefined; + country?: string | undefined; + phone?: string | undefined; + website?: string | undefined; + linkedInUrl?: string | undefined; + }; + jobExperiences: ResolvedJobExperience[]; + education: ResolvedEducation[]; +} + +@Injectable() +export class ImportJobListener { + private readonly logger = new Logger(ImportJobListener.name); + + constructor( + private readonly prisma: PrismaService, + private readonly entityResolver: EntityResolverService, + private readonly profileService: ProfileService, + ) {} + + @OnEvent(UserFileCreatedEvent.eventName, { async: true }) + async handleUserFileCreated(event: UserFileCreatedEvent): Promise { + const job = await this.prisma.importJob.create({ + data: { + userFileId: event.payload.userFileId, + source: event.payload.source.name, + status: "pending", + statusMessage: "Queued", + }, + }); + + await this.processJob(job.id, event); + } + + private async processJob( + jobId: string, + event: UserFileCreatedEvent, + ): Promise { + const { userFileId, user, source, params } = event.payload; + const tag = `job=${jobId} source=${source.name}`; + + try { + await this.startJob(jobId, userFileId); + + const userFile = await this.prisma.userFile.findUniqueOrThrow({ + where: { id: userFileId }, + select: { profileId: true }, + }); + + const onStatus = async (message: string) => { + this.logger.log(`${tag} ${message}`); + await this.updateStatusMessage(jobId, userFileId, message); + }; + + const parsed: ParsedCVData = await source.execute(user, params, onStatus); + + this.logger.log( + `${tag} Parsed: ${parsed.jobExperiences.length} job(s), ` + + `${parsed.education.length} education(s), ${parsed.skills.length} skill(s)`, + ); + this.logger.debug(`${tag} Parsed result: ${JSON.stringify(parsed, null, 2)}`); + + await this.updateStatusMessage(jobId, userFileId, "Resolving entities"); + const resolved = await this.resolveEntities(parsed); + + if (parsed.personalInfo) { + resolved.personalInfo = parsed.personalInfo; + } + + this.logger.log(`${tag} Entity resolution complete`); + + await this.populateProfile(userFile.profileId, parsed, tag); + await this.completeJob(jobId, userFileId, resolved); + } catch (err) { + const message = err instanceof Error ? err.message : "Processing failed"; + this.logger.error(`${tag} Import failed: ${message}`, err instanceof Error ? err.stack : undefined); + await this.failJob(jobId, userFileId, message); + } + } + + private async resolveEntities(parsed: ParsedCVData): Promise { + const [jobExperiences, education] = await Promise.all([ + Promise.all( + parsed.jobExperiences.map(async (job) => { + const [company, role, level, skills] = await Promise.all([ + this.entityResolver.resolveCompany(job.companyName), + this.entityResolver.resolveRole(job.roleName), + this.entityResolver.resolveLevel(job.levelName), + this.entityResolver.resolveSkills(job.skills ?? []), + ]); + + return { + company, + role, + level, + skills, + startDate: new Date(job.startDate), + endDate: job.endDate ? new Date(job.endDate) : null, + description: job.description ?? null, + }; + }), + ), + Promise.all( + parsed.education.map(async (edu) => { + const [institution, skills] = await Promise.all([ + this.entityResolver.resolveInstitution(edu.institutionName), + this.entityResolver.resolveSkills(edu.skills ?? []), + ]); + + return { + institution, + degree: edu.degree, + fieldOfStudy: edu.fieldOfStudy ?? null, + skills, + startDate: new Date(edu.startDate), + endDate: edu.endDate ? new Date(edu.endDate) : null, + description: edu.description ?? null, + }; + }), + ), + ]); + + return { jobExperiences, education }; + } + + /** + * Auto-populate empty profile fields from imported personalInfo. + * Only fills fields that are currently empty -- never overwrites existing data. + */ + private async populateProfile( + profileId: string, + parsed: ParsedCVData, + tag: string, + ): Promise { + const info = parsed.personalInfo; + if (!info) return; + + try { + const existing = await this.profileService.findByIdOrFail(profileId); + const updates: Record = {}; + + const fieldMap: Record = { + fullName: info.name, + headline: info.headline, + summary: info.introduction, + city: info.city, + country: info.country, + phone: info.phone, + website: info.website, + linkedInUrl: info.linkedInUrl, + }; + + for (const [field, value] of Object.entries(fieldMap)) { + if (value && !existing[field as keyof typeof existing]) { + updates[field] = value; + } + } + + if (Object.keys(updates).length === 0) { + this.logger.debug(`${tag} Profile already has data, skipping auto-populate`); + return; + } + + await this.profileService.updateProfile(profileId, updates); + this.logger.log(`${tag} Auto-populated profile: ${Object.keys(updates).join(", ")}`); + } catch (err) { + this.logger.warn(`${tag} Failed to auto-populate profile (non-fatal): ${err instanceof Error ? err.message : err}`); + } + } + + private async startJob(jobId: string, userFileId: string): Promise { + await Promise.all([ + this.prisma.importJob.update({ + where: { id: jobId }, + data: { + status: "processing", + statusMessage: "Starting import", + startedAt: new Date(), + }, + }), + this.prisma.userFile.update({ + where: { id: userFileId }, + data: { status: "processing", statusMessage: "Starting import" }, + }), + ]); + } + + private async updateStatusMessage( + jobId: string, + userFileId: string, + message: string, + ): Promise { + await Promise.all([ + this.prisma.importJob.update({ + where: { id: jobId }, + data: { statusMessage: message }, + }), + this.prisma.userFile.update({ + where: { id: userFileId }, + data: { statusMessage: message }, + }), + ]); + } + + private async completeJob( + jobId: string, + userFileId: string, + result: ResolvedResult, + ): Promise { + await Promise.all([ + this.prisma.importJob.update({ + where: { id: jobId }, + data: { + status: "completed", + statusMessage: "Import completed", + completedAt: new Date(), + }, + }), + this.prisma.userFile.update({ + where: { id: userFileId }, + data: { + status: "completed", + statusMessage: "Import completed", + resultJson: JSON.stringify(result), + }, + }), + ]); + } + + private async failJob( + jobId: string, + userFileId: string, + error: string, + ): Promise { + await Promise.all([ + this.prisma.importJob.update({ + where: { id: jobId }, + data: { + status: "failed", + statusMessage: "Import failed", + error, + completedAt: new Date(), + }, + }), + this.prisma.userFile.update({ + where: { id: userFileId }, + data: { status: "failed", statusMessage: "Import failed", error }, + }), + ]); + } +} diff --git a/packages/core/src/modules/data-import/sources/file-import-source.ts b/packages/core/src/modules/data-import/sources/file-import-source.ts new file mode 100644 index 0000000..7ad9276 --- /dev/null +++ b/packages/core/src/modules/data-import/sources/file-import-source.ts @@ -0,0 +1,150 @@ +import { + CVParserService as CVParser, + type ExistingUserContext, + type ParsedCVData, +} from "@cv/ai-parser"; +import type { User } from "../../auth"; +import { PrismaService } from "../../database"; +import { TextExtractorRegistry, TEXT_EXTRACTOR_REGISTRY } from "@cv/file-upload"; +import { Inject, Injectable, Logger } from "@nestjs/common"; +import { EducationService } from "../../education/education.service"; +import { AIProviderResolverService } from "../../cv-parser/ai-provider-resolver.service"; +import { UserJobExperienceService } from "../../job-experience/employment/user-job-experience.service"; +import { ProfileService } from "../../profile/profile.service"; +import type { DataImportSource } from "../data-import-source.interface"; + +/** + * Build ExistingUserContext from domain models for AI prompt enrichment. + */ +const buildExistingUserContext = ( + profile: { fullName?: string | null; headline?: string | null } | null, + jobs: Array<{ company: { name: string }; role: { name: string }; startDate: Date; endDate?: Date }>, + educations: Array<{ institution: { name: string }; degree: string; startDate: Date; endDate: Date | null }>, +): ExistingUserContext | undefined => { + const context: ExistingUserContext = {}; + + if (profile?.fullName) context.name = profile.fullName; + if (profile?.headline) context.headline = profile.headline; + + if (jobs.length > 0) { + context.jobs = jobs.map((j) => { + const entry: { company: string; role: string; startDate: string; endDate?: string } = { + company: j.company.name, + role: j.role.name, + startDate: j.startDate.toISOString().slice(0, 10), + }; + if (j.endDate) entry.endDate = j.endDate.toISOString().slice(0, 10); + return entry; + }); + } + + if (educations.length > 0) { + context.education = educations.map((e) => { + const entry: { institution: string; degree: string; startDate: string; endDate?: string } = { + institution: e.institution.name, + degree: e.degree, + startDate: e.startDate.toISOString().slice(0, 10), + }; + if (e.endDate) entry.endDate = e.endDate.toISOString().slice(0, 10); + return entry; + }); + } + + const hasData = Object.keys(context).length > 0; + return hasData ? context : undefined; +}; + +/** + * Wraps the existing file extraction + AI parsing pipeline + * as a DataImportSource. + */ +@Injectable() +export class FileImportSource implements DataImportSource { + readonly name = "file-upload"; + private readonly logger = new Logger(FileImportSource.name); + + constructor( + @Inject(TEXT_EXTRACTOR_REGISTRY) + private readonly textExtractorRegistry: TextExtractorRegistry, + private readonly providerResolver: AIProviderResolverService, + private readonly profileService: ProfileService, + private readonly jobExperienceService: UserJobExperienceService, + private readonly educationService: EducationService, + private readonly prisma: PrismaService, + ) {} + + async execute( + user: User, + params: Record, + onStatus: (message: string) => Promise, + ): Promise { + const buffer = params["buffer"] as Buffer; + const mimeType = params["mimeType"] as string; + + await onStatus("Extracting text from file"); + + const extraction = await this.textExtractorRegistry.extract( + buffer, + mimeType, + ); + + if (!extraction.success) { + throw new Error(`Text extraction failed: ${extraction.error}`); + } + + if (!extraction.text || extraction.text.trim().length === 0) { + throw new Error("Could not extract any text from the file"); + } + + this.logger.log(`Extracted ${extraction.text.length} chars from ${mimeType}`); + this.logger.debug(`Extracted text preview: ${extraction.text.slice(0, 500)}`); + + await onStatus("Gathering existing profile data"); + + const existingContext = await this.gatherUserContext(user); + + await onStatus("Analyzing with AI"); + + const provider = await this.providerResolver.resolveForUser(user); + this.logger.log(`AI provider: ${provider.constructor.name}`); + + const parser = new CVParser(provider); + const result = await parser.parseCVText(extraction.text, existingContext); + + return result; + } + + private async gatherUserContext(user: User): Promise { + try { + const firstProfile = await this.prisma.profile.findFirst({ + where: { userId: user.id }, + select: { id: true }, + }); + + if (!firstProfile) return undefined; + + const [profile, jobs, educationResult] = await Promise.all([ + this.profileService.findByIdOrFail(firstProfile.id), + this.jobExperienceService.findForProfile(firstProfile.id), + this.educationService.findManyForProfile(firstProfile.id), + ]); + + const context = buildExistingUserContext( + profile, + jobs, + educationResult.edges.map((e) => e.node), + ); + + if (context) { + this.logger.debug(`User context: ${JSON.stringify(context)}`); + } + + return context; + } catch (err) { + this.logger.warn( + `Failed to gather user context (non-fatal): ${err instanceof Error ? err.message : err}`, + ); + return undefined; + } + } +} diff --git a/packages/core/src/modules/data-import/user-file.entity.ts b/packages/core/src/modules/data-import/user-file.entity.ts new file mode 100644 index 0000000..4e353cd --- /dev/null +++ b/packages/core/src/modules/data-import/user-file.entity.ts @@ -0,0 +1,21 @@ +import { BaseEntity } from "../../shared"; + +export class UserFile extends BaseEntity { + constructor( + id: string, + public readonly profileId: string, + public readonly fingerprint: string | null, + public readonly fileName: string, + public readonly mimeType: string, + public readonly sizeBytes: number, + public readonly source: string, + public readonly status: string, + public readonly statusMessage: string, + public readonly resultJson: string | null, + public readonly error: string | null, + createdAt: Date, + updatedAt: Date, + ) { + super(id, createdAt, updatedAt); + } +} diff --git a/packages/core/src/modules/data-import/user-file.mapper.ts b/packages/core/src/modules/data-import/user-file.mapper.ts new file mode 100644 index 0000000..935eda9 --- /dev/null +++ b/packages/core/src/modules/data-import/user-file.mapper.ts @@ -0,0 +1,39 @@ +import type { BaseMapper } from "../../shared"; +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; + +type PrismaUserFile = Prisma.UserFileGetPayload; + +import { UserFile } from "./user-file.entity"; + +@Injectable() +export class UserFileMapper implements BaseMapper { + toDomain(prisma: null): null; + toDomain(prisma: PrismaUserFile): UserFile; + toDomain(prisma: PrismaUserFile | null): UserFile | null; + toDomain(prisma: PrismaUserFile | null): UserFile | null { + if (!prisma) return null; + + return new UserFile( + prisma.id, + prisma.profileId, + prisma.fingerprint, + prisma.fileName, + prisma.mimeType, + prisma.sizeBytes, + prisma.source, + prisma.status, + prisma.statusMessage, + prisma.resultJson, + prisma.error, + prisma.createdAt, + prisma.updatedAt, + ); + } + + mapToDomain(items: PrismaUserFile[]): UserFile[] { + return items + .map((item) => this.toDomain(item)) + .filter((item): item is UserFile => item !== null); + } +} diff --git a/packages/core/src/modules/data-import/user-file.policy.ts b/packages/core/src/modules/data-import/user-file.policy.ts new file mode 100644 index 0000000..10efd0f --- /dev/null +++ b/packages/core/src/modules/data-import/user-file.policy.ts @@ -0,0 +1,12 @@ +import { Policy, ProfileOwnedResourcePolicy } from "../auth"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import { UserFile } from "./user-file.entity"; + +@Injectable() +@Policy(UserFile) +export class UserFilePolicy extends ProfileOwnedResourcePolicy { + constructor(prisma: PrismaService) { + super(prisma); + } +} diff --git a/packages/system/src/database/database.module.ts b/packages/core/src/modules/database/database.module.ts similarity index 100% rename from packages/system/src/database/database.module.ts rename to packages/core/src/modules/database/database.module.ts diff --git a/packages/system/src/database/index.ts b/packages/core/src/modules/database/index.ts similarity index 100% rename from packages/system/src/database/index.ts rename to packages/core/src/modules/database/index.ts diff --git a/packages/system/src/database/prisma.service.ts b/packages/core/src/modules/database/prisma.service.ts similarity index 100% rename from packages/system/src/database/prisma.service.ts rename to packages/core/src/modules/database/prisma.service.ts diff --git a/packages/core/src/modules/database/seed/seed.module.ts b/packages/core/src/modules/database/seed/seed.module.ts new file mode 100644 index 0000000..23f662a --- /dev/null +++ b/packages/core/src/modules/database/seed/seed.module.ts @@ -0,0 +1,24 @@ +import { DatabaseModule } from ".."; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { DiscoveryModule } from "@nestjs/core"; +import { AuthorizationModule } from "../../auth/authorization/authorization.module"; +import { TokenModule } from "../../auth/token/token.module"; +import { UserModule } from "../../auth/user/user.module"; +import { CVTemplateSeedService } from "../../cv-template/seed/cv-template.seed"; +import { UserSeedService } from "../../user/seed/user.seed"; +import { SeedService } from "./seed.service"; + +@Module({ + imports: [ + ConfigModule.forRoot(), + DatabaseModule, + DiscoveryModule, + UserModule, + TokenModule, + AuthorizationModule, + ], + providers: [SeedService, UserSeedService, CVTemplateSeedService], + exports: [SeedService], +}) +export class SeedModule {} diff --git a/packages/core/src/modules/database/seed/seed.service.ts b/packages/core/src/modules/database/seed/seed.service.ts new file mode 100644 index 0000000..abcf5b2 --- /dev/null +++ b/packages/core/src/modules/database/seed/seed.service.ts @@ -0,0 +1,104 @@ +import { PrismaService } from ".."; +import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; +import { DiscoveryService } from "@nestjs/core"; +import type { SeederConfig } from "./seeder.decorator"; +import { SEEDER_METADATA_KEY } from "./seeder.decorator"; +import { topologicalSort } from "./topological-sort"; + +export interface Seeder { + seed(prisma: PrismaService): Promise; +} + +const parseMetadata = ( + metadata: unknown, + fallbackName: string, +): { name: string; dependsOn: string[] } => { + if (typeof metadata === "string") return { name: metadata, dependsOn: [] }; + if (typeof metadata === "object" && metadata !== null) { + const config = metadata as SeederConfig; + return { + name: config.name ?? fallbackName, + dependsOn: config.dependsOn ?? [], + }; + } + return { name: fallbackName, dependsOn: [] }; +}; + +@Injectable() +export class SeedService implements OnModuleInit { + private readonly logger = new Logger(SeedService.name); + private readonly seeders: Array<{ + name: string; + dependsOn: string[]; + seeder: Seeder; + }> = []; + + constructor( + private readonly prisma: PrismaService, + private readonly discoveryService: DiscoveryService, + ) {} + + async onModuleInit(): Promise { + const providers = this.discoveryService.getProviders(); + + for (const wrapper of providers) { + if (!wrapper.isDependencyTreeStatic() || !wrapper.instance) continue; + + const instance = wrapper.instance; + const seederMetadata = Reflect.getMetadata( + SEEDER_METADATA_KEY, + instance.constructor, + ); + + if (!seederMetadata || typeof instance.seed !== "function") continue; + + const { name, dependsOn } = parseMetadata( + seederMetadata, + instance.constructor.name, + ); + this.seeders.push({ name, dependsOn, seeder: instance }); + this.logger.log(`Registered seeder: ${name}`); + } + } + + async seed(): Promise { + this.logger.log("Starting database seeding..."); + + const result = topologicalSort( + this.seeders.map((s) => ({ + name: s.name, + dependsOn: s.dependsOn, + value: s.seeder, + })), + ); + + if ("cycle" in result) { + throw new Error( + `Circular seeder dependencies detected: ${result.cycle.join(" -> ")}`, + ); + } + + this.logger.log( + `Execution order: ${result.sorted.map((s) => s.name).join(" -> ")}`, + ); + + const failures: string[] = []; + + for (const { name, value: seeder } of result.sorted) { + try { + this.logger.log(`Seeding: ${name}...`); + await seeder.seed(this.prisma); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.logger.error(`Seeder "${name}" failed: ${message}`); + failures.push(name); + } + } + + if (failures.length > 0) { + throw new Error(`Seeders failed: ${failures.join(", ")}`); + } + + this.logger.log("Database seeding completed!"); + } +} diff --git a/packages/core/src/modules/database/seed/seeder.decorator.ts b/packages/core/src/modules/database/seed/seeder.decorator.ts new file mode 100644 index 0000000..ae72ef9 --- /dev/null +++ b/packages/core/src/modules/database/seed/seeder.decorator.ts @@ -0,0 +1,11 @@ +import { SetMetadata } from "@nestjs/common"; + +export const SEEDER_METADATA_KEY = "seeder"; + +export interface SeederConfig { + name?: string; + dependsOn?: string[]; +} + +export const Seeder = (config?: string | SeederConfig) => + SetMetadata(SEEDER_METADATA_KEY, config ?? true); diff --git a/packages/core/src/modules/database/seed/topological-sort.spec.ts b/packages/core/src/modules/database/seed/topological-sort.spec.ts new file mode 100644 index 0000000..dafdef8 --- /dev/null +++ b/packages/core/src/modules/database/seed/topological-sort.spec.ts @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; +import { topologicalSort } from "./topological-sort"; + +describe("topologicalSort", () => { + it("returns empty array for empty input", () => { + const result = topologicalSort([]); + expect(result).toEqual({ sorted: [] }); + }); + + it("returns nodes with no dependencies in stable order", () => { + const result = topologicalSort([ + { name: "A", dependsOn: [], value: 1 }, + { name: "B", dependsOn: [], value: 2 }, + { name: "C", dependsOn: [], value: 3 }, + ]); + + expect(result).toEqual({ + sorted: [ + { name: "A", value: 1 }, + { name: "B", value: 2 }, + { name: "C", value: 3 }, + ], + }); + }); + + it("sorts a linear dependency chain", () => { + const result = topologicalSort([ + { name: "C", dependsOn: ["B"], value: 3 }, + { name: "A", dependsOn: [], value: 1 }, + { name: "B", dependsOn: ["A"], value: 2 }, + ]); + + expect("sorted" in result).toBe(true); + if (!("sorted" in result)) return; + + const names = result.sorted.map((n) => n.name); + expect(names.indexOf("A")).toBeLessThan(names.indexOf("B")); + expect(names.indexOf("B")).toBeLessThan(names.indexOf("C")); + }); + + it("sorts branching dependencies", () => { + const result = topologicalSort([ + { name: "D", dependsOn: ["B", "C"], value: 4 }, + { name: "B", dependsOn: ["A"], value: 2 }, + { name: "C", dependsOn: ["A"], value: 3 }, + { name: "A", dependsOn: [], value: 1 }, + ]); + + expect("sorted" in result).toBe(true); + if (!("sorted" in result)) return; + + const names = result.sorted.map((n) => n.name); + expect(names.indexOf("A")).toBeLessThan(names.indexOf("B")); + expect(names.indexOf("A")).toBeLessThan(names.indexOf("C")); + expect(names.indexOf("B")).toBeLessThan(names.indexOf("D")); + expect(names.indexOf("C")).toBeLessThan(names.indexOf("D")); + }); + + it("detects circular dependencies", () => { + const result = topologicalSort([ + { name: "A", dependsOn: ["B"], value: 1 }, + { name: "B", dependsOn: ["A"], value: 2 }, + ]); + + expect("cycle" in result).toBe(true); + if (!("cycle" in result)) return; + + expect(result.cycle).toContain("A"); + expect(result.cycle).toContain("B"); + }); + + it("throws on unknown dependency names", () => { + expect(() => + topologicalSort([ + { name: "A", dependsOn: ["NonExistent"], value: 1 }, + ]), + ).toThrow("Unknown seeder dependencies: NonExistent"); + }); + + it("preserves values through sorting", () => { + const obj = { data: "test" }; + const result = topologicalSort([ + { name: "A", dependsOn: [], value: obj }, + ]); + + expect("sorted" in result).toBe(true); + if (!("sorted" in result)) return; + + expect(result.sorted[0].value).toBe(obj); + }); +}); diff --git a/packages/core/src/modules/database/seed/topological-sort.ts b/packages/core/src/modules/database/seed/topological-sort.ts new file mode 100644 index 0000000..448b181 --- /dev/null +++ b/packages/core/src/modules/database/seed/topological-sort.ts @@ -0,0 +1,61 @@ +/** + * Topologically sorts nodes using Kahn's algorithm. + * Returns sorted order or the cycle that prevents resolution. + */ +export const topologicalSort = ( + nodes: Array<{ name: string; dependsOn: string[]; value: T }>, +): { sorted: Array<{ name: string; value: T }> } | { cycle: string[] } => { + const nameSet = new Set(nodes.map((n) => n.name)); + + const unknownDeps = nodes.flatMap((n) => + n.dependsOn.filter((dep) => !nameSet.has(dep)), + ); + if (unknownDeps.length > 0) { + throw new Error( + `Unknown seeder dependencies: ${[...new Set(unknownDeps)].join(", ")}`, + ); + } + + const inDegree = new Map(); + const adjacency = new Map(); + const valueMap = new Map(); + + nodes.forEach((n) => { + inDegree.set(n.name, 0); + adjacency.set(n.name, []); + valueMap.set(n.name, n.value); + }); + + nodes.forEach((n) => { + n.dependsOn.forEach((dep) => { + adjacency.get(dep)!.push(n.name); + inDegree.set(n.name, inDegree.get(n.name)! + 1); + }); + }); + + const queue = nodes + .filter((n) => inDegree.get(n.name) === 0) + .map((n) => n.name); + + const sorted: Array<{ name: string; value: T }> = []; + + while (queue.length > 0) { + const current = queue.shift()!; + sorted.push({ name: current, value: valueMap.get(current)! }); + + for (const neighbor of adjacency.get(current)!) { + const newDegree = inDegree.get(neighbor)! - 1; + inDegree.set(neighbor, newDegree); + if (newDegree === 0) queue.push(neighbor); + } + } + + if (sorted.length !== nodes.length) { + const cycle = nodes + .filter((n) => inDegree.get(n.name)! > 0) + .map((n) => n.name); + return { cycle }; + } + + return { sorted }; +}; diff --git a/packages/core/src/modules/education/education.entity.ts b/packages/core/src/modules/education/education.entity.ts new file mode 100644 index 0000000..e478690 --- /dev/null +++ b/packages/core/src/modules/education/education.entity.ts @@ -0,0 +1,24 @@ +import { BaseEntity } from "../../shared"; +import type { Skill } from "../job-experience/skill/skill.entity"; +import { Institution } from "./institution.entity"; + +export class Education extends BaseEntity { + constructor( + id: string, + public readonly profileId: string, + public readonly institution: Institution, + public readonly degree: string, + public readonly fieldOfStudy: string | null, + public readonly startDate: Date, + public readonly endDate: Date | null, + public readonly description: string | null, + createdAt: Date, + updatedAt: Date, + public readonly skills?: Skill[], + ) { + super(id, createdAt, updatedAt); + if (skills !== undefined) { + this.skills = skills; + } + } +} diff --git a/packages/core/src/modules/education/education.mapper.ts b/packages/core/src/modules/education/education.mapper.ts new file mode 100644 index 0000000..e467faf --- /dev/null +++ b/packages/core/src/modules/education/education.mapper.ts @@ -0,0 +1,64 @@ +import type { BaseMapper } from "../../shared"; +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; +import { SkillMapper } from "../job-experience/skill/skill.mapper"; +import { Education } from "./education.entity"; +import { InstitutionMapper } from "./institution.mapper"; + +type PrismaEducation = Prisma.EducationGetPayload<{ + include: { + institution: true; + profile: true; + skills: true; + }; +}>; + +@Injectable() +export class EducationMapper implements BaseMapper { + constructor( + private readonly institutionMapper: InstitutionMapper, + private readonly skillMapper: SkillMapper, + ) {} + + /** + * Maps a Prisma Education entity to a domain Education entity + * Uses overloads to return the correct type based on input + */ + toDomain(prismaEducation: null): null; + toDomain(prismaEducation: PrismaEducation): Education; + toDomain(prismaEducation: PrismaEducation | null): Education | null; + toDomain(prismaEducation: PrismaEducation | null): Education | null { + if (!prismaEducation) { + return null; + } + + const institution = this.institutionMapper.toDomain( + prismaEducation.institution, + ); + if (!institution) { + throw new Error("Institution is required for Education"); + } + + return new Education( + prismaEducation.id, + prismaEducation.profileId, + institution, + prismaEducation.degree, + prismaEducation.fieldOfStudy, + prismaEducation.startDate, + prismaEducation.endDate, + prismaEducation.description, + prismaEducation.createdAt, + prismaEducation.updatedAt, + prismaEducation.skills + ? this.skillMapper.mapToDomain(prismaEducation.skills) + : undefined, + ); + } + + mapToDomain(prismaEducations: PrismaEducation[]): Education[] { + return prismaEducations + .map((education) => this.toDomain(education)) + .filter((education): education is Education => education !== null); + } +} diff --git a/packages/core/src/modules/education/education.module.ts b/packages/core/src/modules/education/education.module.ts new file mode 100644 index 0000000..8104255 --- /dev/null +++ b/packages/core/src/modules/education/education.module.ts @@ -0,0 +1,36 @@ +import { AuthorizationModule } from "../auth"; +import { BaseModule } from "../../shared"; +import { DatabaseModule } from "../database"; +import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "../authentication/authentication.module"; +import { SkillModule } from "../job-experience/skill/skill.module"; +import { EducationMapper } from "./education.mapper"; +import { EducationPolicy } from "./education.policy"; +import { EducationService } from "./education.service"; +import { InstitutionFactory } from "./institution.factory"; +import { InstitutionMapper } from "./institution.mapper"; +import { InstitutionPolicy } from "./institution.policy"; +import { InstitutionService } from "./institution.service"; +import { EducationOnboardingStep } from "./onboarding/education.step"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + SkillModule, + ], + providers: [ + EducationService, + EducationMapper, + EducationPolicy, + InstitutionService, + InstitutionMapper, + InstitutionFactory, + InstitutionPolicy, + EducationOnboardingStep, + ], + exports: [EducationService, EducationMapper, InstitutionService], +}) +export class EducationModule {} diff --git a/packages/core/src/modules/education/education.policy.ts b/packages/core/src/modules/education/education.policy.ts new file mode 100644 index 0000000..6c54f80 --- /dev/null +++ b/packages/core/src/modules/education/education.policy.ts @@ -0,0 +1,12 @@ +import { Policy, ProfileOwnedResourcePolicy } from "../auth"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import { Education } from "./education.entity"; + +@Injectable() +@Policy(Education) +export class EducationPolicy extends ProfileOwnedResourcePolicy { + constructor(prisma: PrismaService) { + super(prisma); + } +} diff --git a/packages/core/src/modules/education/education.service.ts b/packages/core/src/modules/education/education.service.ts new file mode 100644 index 0000000..a932bce --- /dev/null +++ b/packages/core/src/modules/education/education.service.ts @@ -0,0 +1,171 @@ +import { notFound } from "../auth"; +import type { PaginationOptions, PaginationResult } from "../../shared"; +import { PaginationService } from "../../shared"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; +import { Education } from "./education.entity"; +import { EducationMapper } from "./education.mapper"; + +@Injectable() +export class EducationService { + private readonly educationInclude = { + institution: true, + profile: true, + skills: true, + } satisfies Prisma.EducationInclude; + + constructor( + private readonly prisma: PrismaService, + private readonly educationMapper: EducationMapper, + private readonly paginationService: PaginationService, + ) {} + + async findMany( + profileId?: string, + options: PaginationOptions = {}, + ): Promise> { + const where: Prisma.EducationWhereInput = profileId ? { profileId } : {}; + const queryOptions = this.paginationService.buildQueryOptions( + where, + { startDate: "desc" }, + options, + ); + + const [items, totalCount] = await Promise.all([ + this.prisma.education.findMany({ + ...queryOptions, + include: this.educationInclude, + }), + this.count(profileId), + ]); + + const domainEducations = this.educationMapper.mapToDomain(items); + return this.paginationService.buildPaginationResult( + domainEducations, + totalCount, + options, + ); + } + + async findManyForProfile( + profileId: string, + options: PaginationOptions = {}, + ): Promise> { + return this.findMany(profileId, options); + } + + async findById(id: string): Promise { + const education = await this.prisma.education.findUnique({ + where: { id }, + include: this.educationInclude, + }); + return this.educationMapper.toDomain(education); + } + + async count(profileId?: string): Promise { + const where: Prisma.EducationWhereInput = profileId ? { profileId } : {}; + return this.prisma.education.count({ + where, + }); + } + + async create( + profileId: string, + data: { + institutionId: string; + degree: string; + fieldOfStudy?: string | null; + startDate: Date; + endDate?: Date | null; + description?: string | null; + skillIds?: string[]; + }, + ): Promise { + const createData: Prisma.EducationCreateInput = { + profile: { connect: { id: profileId } }, + institution: { connect: { id: data.institutionId } }, + degree: data.degree, + fieldOfStudy: data.fieldOfStudy ?? null, + startDate: data.startDate, + endDate: data.endDate ?? null, + description: data.description ?? null, + }; + + if (data.skillIds && data.skillIds.length > 0) { + createData.skills = { + connect: data.skillIds.map((id) => ({ id })), + }; + } + + const education = await this.prisma.education.create({ + data: createData, + include: this.educationInclude, + }); + const domainEducation = this.educationMapper.toDomain(education); + if (!domainEducation) { + throw new Error("Failed to create education"); + } + return domainEducation; + } + + async findByIdOrFail(id: string): Promise { + const education = await this.findById(id); + return education ?? notFound("Education", "id", id); + } + + async update( + id: string, + data: { + institutionId?: string; + degree?: string; + fieldOfStudy?: string | null; + startDate?: Date; + endDate?: Date | null; + description?: string | null; + skillIds?: string[]; + }, + ): Promise { + const updateData: Prisma.EducationUpdateInput = {}; + + if (data.institutionId !== undefined) { + updateData.institution = { connect: { id: data.institutionId } }; + } + if (data.degree !== undefined) { + updateData.degree = data.degree; + } + if (data.fieldOfStudy !== undefined) { + updateData.fieldOfStudy = data.fieldOfStudy; + } + if (data.startDate !== undefined) { + updateData.startDate = data.startDate; + } + if (data.endDate !== undefined) { + updateData.endDate = data.endDate; + } + if (data.description !== undefined) { + updateData.description = data.description; + } + if (data.skillIds !== undefined) { + updateData.skills = { set: data.skillIds.map((sid) => ({ id: sid })) }; + } + + const updated = await this.prisma.education.update({ + where: { id }, + data: updateData, + include: this.educationInclude, + }); + + const domainEducation = this.educationMapper.toDomain(updated); + if (!domainEducation) { + throw new Error("Failed to update education"); + } + return domainEducation; + } + + async delete(id: string): Promise { + await this.prisma.education.delete({ + where: { id }, + }); + } +} diff --git a/packages/core/src/modules/education/institution.entity.ts b/packages/core/src/modules/education/institution.entity.ts new file mode 100644 index 0000000..1f4323a --- /dev/null +++ b/packages/core/src/modules/education/institution.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "../../shared"; + +export class Institution extends NamedEntity {} diff --git a/packages/core/src/modules/education/institution.factory.ts b/packages/core/src/modules/education/institution.factory.ts new file mode 100644 index 0000000..242a152 --- /dev/null +++ b/packages/core/src/modules/education/institution.factory.ts @@ -0,0 +1,29 @@ +import { ClockService, Factory, UuidFactoryService } from "../../shared"; +import { Injectable } from "@nestjs/common"; +import { Institution } from "./institution.entity"; + +type CreateInstitutionDto = { + name: string; + description?: string | null; +}; + +@Injectable() +export class InstitutionFactory + implements Factory +{ + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(dto: CreateInstitutionDto): Institution { + const now = this.clock.now(); + return new Institution( + this.uuidFactory.generate(), + dto.name, + now, + now, + dto.description ?? undefined, + ); + } +} diff --git a/packages/core/src/modules/education/institution.mapper.ts b/packages/core/src/modules/education/institution.mapper.ts new file mode 100644 index 0000000..4e2be02 --- /dev/null +++ b/packages/core/src/modules/education/institution.mapper.ts @@ -0,0 +1,22 @@ +import { createNamedEntityMapper } from "../../shared"; +import { Injectable } from "@nestjs/common"; +import type { Institution as PrismaInstitution } from "@prisma/client"; +import { Institution } from "./institution.entity"; + +@Injectable() +export class InstitutionMapper { + private mapper = createNamedEntityMapper( + Institution, + ); + + toDomain(prismaInstitution: null): null; + toDomain(prismaInstitution: PrismaInstitution): Institution; + toDomain(prismaInstitution: PrismaInstitution | null): Institution | null; + toDomain(prismaInstitution: PrismaInstitution | null): Institution | null { + return this.mapper.toDomain(prismaInstitution); + } + + mapToDomain(prismaInstitutions: PrismaInstitution[]): Institution[] { + return this.mapper.mapToDomain(prismaInstitutions); + } +} diff --git a/packages/core/src/modules/education/institution.policy.ts b/packages/core/src/modules/education/institution.policy.ts new file mode 100644 index 0000000..b9872aa --- /dev/null +++ b/packages/core/src/modules/education/institution.policy.ts @@ -0,0 +1,11 @@ +import { Policy, PublicResourcePolicy } from "../auth"; +import { Injectable } from "@nestjs/common"; +import { Institution } from "./institution.entity"; + +@Injectable() +@Policy(Institution) +export class InstitutionPolicy extends PublicResourcePolicy { + override create(): boolean { + return true; + } +} diff --git a/packages/core/src/modules/education/institution.service.ts b/packages/core/src/modules/education/institution.service.ts new file mode 100644 index 0000000..5405ab7 --- /dev/null +++ b/packages/core/src/modules/education/institution.service.ts @@ -0,0 +1,12 @@ +import { NamedEntityService } from "../../shared"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import { Institution } from "./institution.entity"; +import { InstitutionMapper } from "./institution.mapper"; + +@Injectable() +export class InstitutionService extends NamedEntityService { + constructor(prisma: PrismaService, institutionMapper: InstitutionMapper) { + super(prisma, institutionMapper, prisma.institution, "Institution"); + } +} diff --git a/packages/core/src/modules/education/onboarding/education.step.ts b/packages/core/src/modules/education/onboarding/education.step.ts new file mode 100644 index 0000000..291f229 --- /dev/null +++ b/packages/core/src/modules/education/onboarding/education.step.ts @@ -0,0 +1,28 @@ +import type { User } from "../../auth"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { OnboardingStep } from "../../onboarding/onboarding-step.decorator"; +import { + type OnboardingStepDefinition, + OnboardingStepStatus, +} from "../../onboarding/onboarding-step.interface"; + +@Injectable() +@OnboardingStep() +export class EducationOnboardingStep implements OnboardingStepDefinition { + readonly name = "education"; + readonly order = 4; + readonly dependsOn: string[] = []; + + constructor(private readonly prisma: PrismaService) {} + + async computeStatus(user: User): Promise { + const count = await this.prisma.education.count({ + where: { profile: { userId: user.id } }, + }); + + return count > 0 + ? OnboardingStepStatus.COMPLETE + : OnboardingStepStatus.NOT_STARTED; + } +} diff --git a/packages/core/src/modules/events/domain-event.ts b/packages/core/src/modules/events/domain-event.ts new file mode 100644 index 0000000..063b1cc --- /dev/null +++ b/packages/core/src/modules/events/domain-event.ts @@ -0,0 +1,31 @@ +/** + * Base class for all domain events. + * Provides typed payloads and a consistent event name derived from the class name. + * + * @example + * ```ts + * class VacancyImported extends DomainEvent<{ vacancyId: string; source: string }> {} + * + * // Emit: + * eventService.emit(new VacancyImported({ vacancyId: "123", source: "manual" })); + * + * // Listen: + * @OnEvent(VacancyImported.eventName) + * async handle(event: VacancyImported) { ... } + * ``` + */ +export abstract class DomainEvent { + static get eventName(): string { + return this.name; + } + + readonly eventName: string; + readonly payload: TPayload; + readonly occurredAt: Date; + + constructor(...args: TPayload extends void ? [] : [payload: TPayload]) { + this.eventName = (this.constructor as typeof DomainEvent).eventName; + this.payload = args[0] as TPayload; + this.occurredAt = new Date(); + } +} diff --git a/packages/core/src/modules/events/event.service.ts b/packages/core/src/modules/events/event.service.ts new file mode 100644 index 0000000..6bd04ae --- /dev/null +++ b/packages/core/src/modules/events/event.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from "@nestjs/common"; +import { EventEmitter2 } from "@nestjs/event-emitter"; +import type { DomainEvent } from "./domain-event"; + +/** + * Typed wrapper around NestJS EventEmitter2. + * Use this instead of injecting EventEmitter2 directly for type safety. + */ +@Injectable() +export class EventService { + constructor(private readonly emitter: EventEmitter2) {} + + emit(event: DomainEvent): void { + this.emitter.emit(event.eventName, event); + } + + async emitAsync(event: DomainEvent): Promise { + await this.emitter.emitAsync(event.eventName, event); + } +} diff --git a/packages/core/src/modules/events/events.module.ts b/packages/core/src/modules/events/events.module.ts new file mode 100644 index 0000000..803a556 --- /dev/null +++ b/packages/core/src/modules/events/events.module.ts @@ -0,0 +1,11 @@ +import { Global, Module } from "@nestjs/common"; +import { EventEmitterModule } from "@nestjs/event-emitter"; +import { EventService } from "./event.service"; + +@Global() +@Module({ + imports: [EventEmitterModule.forRoot()], + providers: [EventService], + exports: [EventService], +}) +export class EventsModule {} diff --git a/packages/core/src/modules/events/index.ts b/packages/core/src/modules/events/index.ts new file mode 100644 index 0000000..0cfd546 --- /dev/null +++ b/packages/core/src/modules/events/index.ts @@ -0,0 +1,3 @@ +export { DomainEvent } from "./domain-event"; +export { EventService } from "./event.service"; +export { EventsModule } from "./events.module"; diff --git a/packages/core/src/modules/job-experience/company/company.dataloader.ts b/packages/core/src/modules/job-experience/company/company.dataloader.ts new file mode 100644 index 0000000..d249a63 --- /dev/null +++ b/packages/core/src/modules/job-experience/company/company.dataloader.ts @@ -0,0 +1,23 @@ +import { BaseDataLoaderService } from "../../../shared"; +import { Injectable, Scope } from "@nestjs/common"; +import { Company } from "./company.entity"; +import { CompanyService } from "./company.service"; + +@Injectable({ scope: Scope.REQUEST }) +export class CompanyDataLoaderService extends BaseDataLoaderService< + string, + Company +> { + constructor(readonly companyService: CompanyService) { + super(async (ids: readonly string[]) => { + const companies = await companyService.findMany({ id: [...ids] }); + + const companyMap = new Map(); + for (const company of companies) { + companyMap.set(company.id, company); + } + + return ids.map((id) => companyMap.get(id) ?? null); + }); + } +} diff --git a/packages/core/src/modules/job-experience/company/company.dto.ts b/packages/core/src/modules/job-experience/company/company.dto.ts new file mode 100644 index 0000000..e89771a --- /dev/null +++ b/packages/core/src/modules/job-experience/company/company.dto.ts @@ -0,0 +1,11 @@ +export interface CreateCompanyDto { + name: string; + description?: string; + website?: string; +} + +export interface UpdateCompanyDto { + name?: string; + description?: string; + website?: string; +} diff --git a/packages/core/src/modules/job-experience/company/company.entity.ts b/packages/core/src/modules/job-experience/company/company.entity.ts new file mode 100644 index 0000000..88e203c --- /dev/null +++ b/packages/core/src/modules/job-experience/company/company.entity.ts @@ -0,0 +1,14 @@ +import { NamedEntity } from "../../../shared"; + +export class Company extends NamedEntity { + constructor( + id: string, + name: string, + createdAt: Date, + updatedAt: Date, + description?: string, + public website?: string, + ) { + super(id, name, createdAt, updatedAt, description); + } +} diff --git a/packages/core/src/modules/job-experience/company/company.factory.ts b/packages/core/src/modules/job-experience/company/company.factory.ts new file mode 100644 index 0000000..939c439 --- /dev/null +++ b/packages/core/src/modules/job-experience/company/company.factory.ts @@ -0,0 +1,24 @@ +import { ClockService, Factory, UuidFactoryService } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { CreateCompanyDto } from "./company.dto"; +import { Company } from "./company.entity"; + +@Injectable() +export class CompanyFactory implements Factory { + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(dto: CreateCompanyDto): Company { + const now = this.clock.now(); + return new Company( + this.uuidFactory.generate(), + dto.name, + now, + now, + dto.description, + dto.website, + ); + } +} diff --git a/packages/core/src/modules/job-experience/company/company.mapper.ts b/packages/core/src/modules/job-experience/company/company.mapper.ts new file mode 100644 index 0000000..c5bcb56 --- /dev/null +++ b/packages/core/src/modules/job-experience/company/company.mapper.ts @@ -0,0 +1,27 @@ +import { Injectable } from "@nestjs/common"; +import type { Company as PrismaCompany } from "@prisma/client"; +import { Company } from "./company.entity"; + +@Injectable() +export class CompanyMapper { + toDomain(prismaCompany: null): null; + toDomain(prismaCompany: PrismaCompany): Company; + toDomain(prismaCompany: PrismaCompany | null): Company | null; + toDomain(prismaCompany: PrismaCompany | null): Company | null { + if (prismaCompany === null) { + return null; + } + return new Company( + prismaCompany.id, + prismaCompany.name, + prismaCompany.createdAt, + prismaCompany.updatedAt, + prismaCompany.description ?? undefined, + prismaCompany.website ?? undefined, + ); + } + + mapToDomain(prismaCompanies: PrismaCompany[]): Company[] { + return prismaCompanies.map((company) => this.toDomain(company)); + } +} diff --git a/packages/core/src/modules/job-experience/company/company.module.ts b/packages/core/src/modules/job-experience/company/company.module.ts new file mode 100644 index 0000000..adad9a3 --- /dev/null +++ b/packages/core/src/modules/job-experience/company/company.module.ts @@ -0,0 +1,28 @@ +import { AuthorizationModule } from "../../auth"; +import { BaseModule } from "../../../shared"; +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "../../authentication/authentication.module"; +import { CompanyService } from "./company.service"; +import { CompanyDataLoaderService } from "./company.dataloader"; +import { CompanyFactory } from "./company.factory"; +import { CompanyMapper } from "./company.mapper"; +import { CompanyPolicy } from "./company.policy"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + ], + providers: [ + CompanyService, + CompanyMapper, + CompanyFactory, + CompanyPolicy, + CompanyDataLoaderService, + ], + exports: [CompanyService, CompanyMapper, CompanyDataLoaderService], +}) +export class CompanyModule {} diff --git a/packages/core/src/modules/job-experience/company/company.policy.ts b/packages/core/src/modules/job-experience/company/company.policy.ts new file mode 100644 index 0000000..65df040 --- /dev/null +++ b/packages/core/src/modules/job-experience/company/company.policy.ts @@ -0,0 +1,11 @@ +import { Policy, PublicResourcePolicy } from "../../auth"; +import { Injectable } from "@nestjs/common"; +import { Company } from "./company.entity"; + +@Injectable() +@Policy(Company) +export class CompanyPolicy extends PublicResourcePolicy { + override create(): boolean { + return true; + } +} diff --git a/packages/core/src/modules/job-experience/company/company.service.ts b/packages/core/src/modules/job-experience/company/company.service.ts new file mode 100644 index 0000000..82edc90 --- /dev/null +++ b/packages/core/src/modules/job-experience/company/company.service.ts @@ -0,0 +1,23 @@ +import { NamedEntityService } from "../../../shared"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { Company } from "./company.entity"; +import { CompanyMapper } from "./company.mapper"; + +@Injectable() +export class CompanyService extends NamedEntityService { + constructor(prisma: PrismaService, companyMapper: CompanyMapper) { + super(prisma, companyMapper, prisma.company, "Company"); + } + + protected override buildPrismaData(entity: Company): { + name: string; + description: string | null; + website: string | null; + } { + return { + ...super.buildPrismaData(entity), + website: entity.website ?? null, + }; + } +} diff --git a/packages/core/src/modules/job-experience/employment/employment.module.ts b/packages/core/src/modules/job-experience/employment/employment.module.ts new file mode 100644 index 0000000..dc5cc07 --- /dev/null +++ b/packages/core/src/modules/job-experience/employment/employment.module.ts @@ -0,0 +1,34 @@ +import { AuthorizationModule } from "../../auth"; +import { BaseModule } from "../../../shared"; +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "../../authentication/authentication.module"; +import { CompanyModule } from "../company/company.module"; +import { LevelModule } from "../level/level.module"; +import { RoleModule } from "../role/role.module"; +import { SkillModule } from "../skill/skill.module"; +import { CareerHistoryOnboardingStep } from "./onboarding/career-history.step"; +import { UserJobExperienceMapper } from "./user-job-experience.mapper"; +import { UserJobExperiencePolicy } from "./user-job-experience.policy"; +import { UserJobExperienceService } from "./user-job-experience.service"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + CompanyModule, + RoleModule, + LevelModule, + SkillModule, + ], + providers: [ + UserJobExperienceService, + UserJobExperienceMapper, + UserJobExperiencePolicy, + CareerHistoryOnboardingStep, + ], + exports: [UserJobExperienceService, UserJobExperienceMapper], +}) +export class EmploymentModule {} diff --git a/packages/core/src/modules/job-experience/employment/onboarding/career-history.step.ts b/packages/core/src/modules/job-experience/employment/onboarding/career-history.step.ts new file mode 100644 index 0000000..27d7913 --- /dev/null +++ b/packages/core/src/modules/job-experience/employment/onboarding/career-history.step.ts @@ -0,0 +1,28 @@ +import type { User } from "../../../auth"; +import { PrismaService } from "../../../database"; +import { Injectable } from "@nestjs/common"; +import { OnboardingStep } from "../../../onboarding/onboarding-step.decorator"; +import { + type OnboardingStepDefinition, + OnboardingStepStatus, +} from "../../../onboarding/onboarding-step.interface"; + +@Injectable() +@OnboardingStep() +export class CareerHistoryOnboardingStep implements OnboardingStepDefinition { + readonly name = "career-history"; + readonly order = 3; + readonly dependsOn: string[] = []; + + constructor(private readonly prisma: PrismaService) {} + + async computeStatus(user: User): Promise { + const count = await this.prisma.userJobExperience.count({ + where: { profile: { userId: user.id } }, + }); + + return count > 0 + ? OnboardingStepStatus.COMPLETE + : OnboardingStepStatus.NOT_STARTED; + } +} diff --git a/packages/core/src/modules/job-experience/employment/user-job-experience.dto.ts b/packages/core/src/modules/job-experience/employment/user-job-experience.dto.ts new file mode 100644 index 0000000..bae690d --- /dev/null +++ b/packages/core/src/modules/job-experience/employment/user-job-experience.dto.ts @@ -0,0 +1,25 @@ +import type { Company } from "../company/company.entity"; +import type { Level } from "../level/level.entity"; +import type { Role } from "../role/role.entity"; +import type { Skill } from "../skill/skill.entity"; + +export interface CreateUserJobExperienceDto { + profileId: string; + company: Company; + role: Role; + level: Level; + startDate: Date; + endDate?: Date; + description?: string; + skills?: Skill[]; +} + +export interface UpdateUserJobExperienceDto { + company?: Company; + role?: Role; + level?: Level; + startDate?: Date; + endDate?: Date; + description?: string; + skills?: Skill[]; +} diff --git a/packages/core/src/modules/job-experience/employment/user-job-experience.entity.ts b/packages/core/src/modules/job-experience/employment/user-job-experience.entity.ts new file mode 100644 index 0000000..c46ecf1 --- /dev/null +++ b/packages/core/src/modules/job-experience/employment/user-job-experience.entity.ts @@ -0,0 +1,48 @@ +import { BaseEntity } from "../../../shared"; +import type { Company } from "../company/company.entity"; +import type { Level } from "../level/level.entity"; +import type { Role } from "../role/role.entity"; +import type { Skill } from "../skill/skill.entity"; + +export class UserJobExperience extends BaseEntity { + profileId: string; + startDate: Date; + endDate?: Date; + description?: string; + + // Relations + company: Company; + role: Role; + level: Level; + skills?: Skill[]; + + constructor( + id: string, + profileId: string, + startDate: Date, + createdAt: Date, + updatedAt: Date, + company: Company, + role: Role, + level: Level, + endDate?: Date, + description?: string, + skills?: Skill[], + ) { + super(id, createdAt, updatedAt); + this.profileId = profileId; + this.startDate = startDate; + this.company = company; + this.role = role; + this.level = level; + if (endDate !== undefined) { + this.endDate = endDate; + } + if (description !== undefined) { + this.description = description; + } + if (skills !== undefined) { + this.skills = skills; + } + } +} diff --git a/packages/core/src/modules/job-experience/employment/user-job-experience.mapper.ts b/packages/core/src/modules/job-experience/employment/user-job-experience.mapper.ts new file mode 100644 index 0000000..a0c55ba --- /dev/null +++ b/packages/core/src/modules/job-experience/employment/user-job-experience.mapper.ts @@ -0,0 +1,84 @@ +import type { BaseMapper } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { + Company as PrismaCompany, + Level as PrismaLevel, + Profile as PrismaProfile, + Role as PrismaRole, + Skill as PrismaSkill, + UserJobExperience as PrismaUserJobExperience, +} from "@prisma/client"; +import { CompanyMapper } from "../company/company.mapper"; +import { LevelMapper } from "../level/level.mapper"; +import { RoleMapper } from "../role/role.mapper"; +import { SkillMapper } from "../skill/skill.mapper"; +import { UserJobExperience } from "./user-job-experience.entity"; + +/** + * Extended Prisma entity type that includes relations + */ +type PrismaUserJobExperienceWithRelations = PrismaUserJobExperience & { + company: PrismaCompany; + role: PrismaRole; + level: PrismaLevel; + skills?: PrismaSkill[]; + profile?: PrismaProfile | null; +}; + +/** + * Mapper service for converting between Prisma UserJobExperience entities and domain UserJobExperience entities + */ +@Injectable() +export class UserJobExperienceMapper + implements BaseMapper +{ + constructor( + private companyMapper: CompanyMapper, + private roleMapper: RoleMapper, + private levelMapper: LevelMapper, + private skillMapper: SkillMapper, + ) {} + /** + * Maps a Prisma UserJobExperience entity to a domain UserJobExperience entity + * Uses overloads to return the correct type based on input + */ + toDomain(prismaExperience: null): null; + toDomain( + prismaExperience: PrismaUserJobExperienceWithRelations, + ): UserJobExperience; + toDomain( + prismaExperience: PrismaUserJobExperienceWithRelations | null, + ): UserJobExperience | null; + toDomain( + prismaExperience: PrismaUserJobExperienceWithRelations | null, + ): UserJobExperience | null { + if (prismaExperience === null) { + return null; + } + + return new UserJobExperience( + prismaExperience.id, + prismaExperience.profileId, + prismaExperience.startDate, + prismaExperience.createdAt, + prismaExperience.updatedAt, + this.companyMapper.toDomain(prismaExperience.company), + this.roleMapper.toDomain(prismaExperience.role), + this.levelMapper.toDomain(prismaExperience.level), + prismaExperience.endDate ?? undefined, + prismaExperience.description ?? undefined, + prismaExperience.skills + ? this.skillMapper.mapToDomain(prismaExperience.skills) + : undefined, + ); + } + + /** + * Maps an array of Prisma UserJobExperience entities to domain UserJobExperience entities + */ + mapToDomain( + prismaExperiences: PrismaUserJobExperienceWithRelations[], + ): UserJobExperience[] { + return prismaExperiences.map((experience) => this.toDomain(experience)); + } +} diff --git a/packages/core/src/modules/job-experience/employment/user-job-experience.policy.ts b/packages/core/src/modules/job-experience/employment/user-job-experience.policy.ts new file mode 100644 index 0000000..5c24c24 --- /dev/null +++ b/packages/core/src/modules/job-experience/employment/user-job-experience.policy.ts @@ -0,0 +1,12 @@ +import { Policy, ProfileOwnedResourcePolicy } from "../../auth"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { UserJobExperience } from "./user-job-experience.entity"; + +@Injectable() +@Policy(UserJobExperience) +export class UserJobExperiencePolicy extends ProfileOwnedResourcePolicy { + constructor(prisma: PrismaService) { + super(prisma); + } +} diff --git a/packages/core/src/modules/job-experience/employment/user-job-experience.service.ts b/packages/core/src/modules/job-experience/employment/user-job-experience.service.ts new file mode 100644 index 0000000..a53e681 --- /dev/null +++ b/packages/core/src/modules/job-experience/employment/user-job-experience.service.ts @@ -0,0 +1,224 @@ +import { notFound } from "../../auth"; +import type { PaginationOptions, PaginationResult } from "../../../shared"; +import { PaginationService } from "../../../shared"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; +import type { + CreateUserJobExperienceDto, + UpdateUserJobExperienceDto, +} from "./user-job-experience.dto"; +import { UserJobExperience } from "./user-job-experience.entity"; +import { UserJobExperienceMapper } from "./user-job-experience.mapper"; + +type PrismaUserJobExperienceCreateData = { + profile: { connect: { id: string } }; + company: { connect: { id: string } }; + role: { connect: { id: string } }; + level: { connect: { id: string } }; + startDate: Date; + endDate?: Date; + description?: string; + skills?: { + connect: Array<{ id: string }>; + }; +}; + +type PrismaUserJobExperienceUpdateData = { + company?: { connect: { id: string } }; + role?: { connect: { id: string } }; + level?: { connect: { id: string } }; + startDate?: Date; + endDate?: Date; + description?: string; + skills?: { + set: Array<{ id: string }>; + }; +}; + +@Injectable() +export class UserJobExperienceService { + private readonly userJobExperienceInclude = { + profile: true, + company: true, + role: true, + level: true, + skills: true, + } satisfies Prisma.UserJobExperienceInclude; + + constructor( + private prisma: PrismaService, + private userJobExperienceMapper: UserJobExperienceMapper, + private paginationService: PaginationService, + ) {} + + async create( + createUserJobExperienceDto: CreateUserJobExperienceDto, + ): Promise { + const createData: PrismaUserJobExperienceCreateData = { + profile: { connect: { id: createUserJobExperienceDto.profileId } }, + company: { connect: { id: createUserJobExperienceDto.company.id } }, + role: { connect: { id: createUserJobExperienceDto.role.id } }, + level: { connect: { id: createUserJobExperienceDto.level.id } }, + startDate: createUserJobExperienceDto.startDate, + }; + + if (createUserJobExperienceDto.endDate !== undefined) { + createData.endDate = createUserJobExperienceDto.endDate; + } + if (createUserJobExperienceDto.description !== undefined) { + createData.description = createUserJobExperienceDto.description; + } + if (createUserJobExperienceDto.skills !== undefined) { + createData.skills = { + connect: createUserJobExperienceDto.skills.map((skill) => ({ + id: skill.id, + })), + }; + } + + const prismaExperience = await this.prisma.userJobExperience.create({ + data: createData, + include: this.userJobExperienceInclude, + }); + return this.userJobExperienceMapper.toDomain(prismaExperience); + } + + async findMany( + profileId?: string, + options: PaginationOptions = {}, + ): Promise> { + const where: Prisma.UserJobExperienceWhereInput = profileId + ? { profileId } + : {}; + const queryOptions = this.paginationService.buildQueryOptions( + where, + { startDate: "desc" }, + options, + ); + + const [items, totalCount] = await Promise.all([ + this.prisma.userJobExperience.findMany({ + ...queryOptions, + include: this.userJobExperienceInclude, + }), + this.prisma.userJobExperience.count({ where }), + ]); + + const domainExperiences = this.userJobExperienceMapper.mapToDomain(items); + return this.paginationService.buildPaginationResult( + domainExperiences, + totalCount, + options, + ); + } + + async findForProfile(profileId: string): Promise { + const result = await this.findMany(profileId); + return result.edges.map((edge) => edge.node); + } + + async findManyForProfile( + profileId: string, + options: PaginationOptions = {}, + ): Promise> { + return this.findMany(profileId, options); + } + + async findById(id: string): Promise { + const prismaExperience = await this.prisma.userJobExperience.findUnique({ + where: { id }, + include: this.userJobExperienceInclude, + }); + return this.userJobExperienceMapper.toDomain(prismaExperience); + } + + async findByIdOrFail(id: string): Promise { + const experience = await this.findById(id); + return experience ?? notFound("Job experience", "id", id); + } + + async update( + id: string, + updateUserJobExperienceDto: UpdateUserJobExperienceDto, + ): Promise { + const updateData: PrismaUserJobExperienceUpdateData = {}; + + if (updateUserJobExperienceDto.company !== undefined) { + updateData.company = { + connect: { id: updateUserJobExperienceDto.company.id }, + }; + } + if (updateUserJobExperienceDto.role !== undefined) { + updateData.role = { + connect: { id: updateUserJobExperienceDto.role.id }, + }; + } + if (updateUserJobExperienceDto.level !== undefined) { + updateData.level = { + connect: { id: updateUserJobExperienceDto.level.id }, + }; + } + if (updateUserJobExperienceDto.startDate !== undefined) { + updateData.startDate = updateUserJobExperienceDto.startDate; + } + if (updateUserJobExperienceDto.endDate !== undefined) { + updateData.endDate = updateUserJobExperienceDto.endDate; + } + if (updateUserJobExperienceDto.description !== undefined) { + updateData.description = updateUserJobExperienceDto.description; + } + if (updateUserJobExperienceDto.skills !== undefined) { + updateData.skills = { + set: updateUserJobExperienceDto.skills.map((skill) => ({ + id: skill.id, + })), + }; + } + + const prismaExperience = await this.prisma.userJobExperience.update({ + where: { id }, + data: updateData, + include: this.userJobExperienceInclude, + }); + return this.userJobExperienceMapper.toDomain(prismaExperience); + } + + async delete(id: string): Promise { + await this.prisma.userJobExperience.delete({ + where: { id }, + }); + } + + async addSkills( + experienceId: string, + skillIds: string[], + ): Promise { + const prismaExperience = await this.prisma.userJobExperience.update({ + where: { id: experienceId }, + data: { + skills: { + connect: skillIds.map((id) => ({ id })), + }, + }, + include: this.userJobExperienceInclude, + }); + return this.userJobExperienceMapper.toDomain(prismaExperience); + } + + async removeSkills( + experienceId: string, + skillIds: string[], + ): Promise { + const prismaExperience = await this.prisma.userJobExperience.update({ + where: { id: experienceId }, + data: { + skills: { + disconnect: skillIds.map((id) => ({ id })), + }, + }, + include: this.userJobExperienceInclude, + }); + return this.userJobExperienceMapper.toDomain(prismaExperience); + } +} diff --git a/packages/core/src/modules/job-experience/level/level.dataloader.ts b/packages/core/src/modules/job-experience/level/level.dataloader.ts new file mode 100644 index 0000000..9bc7c10 --- /dev/null +++ b/packages/core/src/modules/job-experience/level/level.dataloader.ts @@ -0,0 +1,23 @@ +import { BaseDataLoaderService } from "../../../shared"; +import { Injectable, Scope } from "@nestjs/common"; +import { Level } from "./level.entity"; +import { LevelService } from "./level.service"; + +@Injectable({ scope: Scope.REQUEST }) +export class LevelDataLoaderService extends BaseDataLoaderService< + string, + Level +> { + constructor(readonly levelService: LevelService) { + super(async (ids: readonly string[]) => { + const levels = await levelService.findMany({ id: [...ids] }); + + const levelMap = new Map(); + for (const level of levels) { + levelMap.set(level.id, level); + } + + return ids.map((id) => levelMap.get(id) ?? null); + }); + } +} diff --git a/packages/core/src/modules/job-experience/level/level.dto.ts b/packages/core/src/modules/job-experience/level/level.dto.ts new file mode 100644 index 0000000..59d1f57 --- /dev/null +++ b/packages/core/src/modules/job-experience/level/level.dto.ts @@ -0,0 +1,9 @@ +export interface CreateLevelDto { + name: string; + description?: string; +} + +export interface UpdateLevelDto { + name?: string; + description?: string; +} diff --git a/packages/core/src/modules/job-experience/level/level.entity.ts b/packages/core/src/modules/job-experience/level/level.entity.ts new file mode 100644 index 0000000..71def10 --- /dev/null +++ b/packages/core/src/modules/job-experience/level/level.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "../../../shared"; + +export class Level extends NamedEntity {} diff --git a/packages/core/src/modules/job-experience/level/level.factory.ts b/packages/core/src/modules/job-experience/level/level.factory.ts new file mode 100644 index 0000000..a0139c5 --- /dev/null +++ b/packages/core/src/modules/job-experience/level/level.factory.ts @@ -0,0 +1,23 @@ +import { ClockService, Factory, UuidFactoryService } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { CreateLevelDto } from "./level.dto"; +import { Level } from "./level.entity"; + +@Injectable() +export class LevelFactory implements Factory { + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(dto: CreateLevelDto): Level { + const now = this.clock.now(); + return new Level( + this.uuidFactory.generate(), + dto.name, + now, + now, + dto.description, + ); + } +} diff --git a/packages/core/src/modules/job-experience/level/level.mapper.ts b/packages/core/src/modules/job-experience/level/level.mapper.ts new file mode 100644 index 0000000..e258928 --- /dev/null +++ b/packages/core/src/modules/job-experience/level/level.mapper.ts @@ -0,0 +1,23 @@ +import { createNamedEntityMapper } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { Level as PrismaLevel } from "@prisma/client"; +import { Level } from "./level.entity"; + +/** + * Mapper service for converting between Prisma Level entities and domain Level entities + */ +@Injectable() +export class LevelMapper { + private mapper = createNamedEntityMapper(Level); + + toDomain(prismaLevel: null): null; + toDomain(prismaLevel: PrismaLevel): Level; + toDomain(prismaLevel: PrismaLevel | null): Level | null; + toDomain(prismaLevel: PrismaLevel | null): Level | null { + return this.mapper.toDomain(prismaLevel); + } + + mapToDomain(prismaLevels: PrismaLevel[]): Level[] { + return this.mapper.mapToDomain(prismaLevels); + } +} diff --git a/packages/core/src/modules/job-experience/level/level.module.ts b/packages/core/src/modules/job-experience/level/level.module.ts new file mode 100644 index 0000000..1ab6d7b --- /dev/null +++ b/packages/core/src/modules/job-experience/level/level.module.ts @@ -0,0 +1,28 @@ +import { AuthorizationModule } from "../../auth"; +import { BaseModule } from "../../../shared"; +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "../../authentication/authentication.module"; +import { LevelService } from "./level.service"; +import { LevelDataLoaderService } from "./level.dataloader"; +import { LevelFactory } from "./level.factory"; +import { LevelMapper } from "./level.mapper"; +import { LevelPolicy } from "./level.policy"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + ], + providers: [ + LevelService, + LevelMapper, + LevelFactory, + LevelPolicy, + LevelDataLoaderService, + ], + exports: [LevelService, LevelMapper, LevelDataLoaderService], +}) +export class LevelModule {} diff --git a/packages/core/src/modules/job-experience/level/level.policy.ts b/packages/core/src/modules/job-experience/level/level.policy.ts new file mode 100644 index 0000000..c51561e --- /dev/null +++ b/packages/core/src/modules/job-experience/level/level.policy.ts @@ -0,0 +1,11 @@ +import { Policy, PublicResourcePolicy } from "../../auth"; +import { Injectable } from "@nestjs/common"; +import { Level } from "./level.entity"; + +@Injectable() +@Policy(Level) +export class LevelPolicy extends PublicResourcePolicy { + override create(): boolean { + return true; + } +} diff --git a/packages/core/src/modules/job-experience/level/level.service.ts b/packages/core/src/modules/job-experience/level/level.service.ts new file mode 100644 index 0000000..f5053dd --- /dev/null +++ b/packages/core/src/modules/job-experience/level/level.service.ts @@ -0,0 +1,12 @@ +import { NamedEntityService } from "../../../shared"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { Level } from "./level.entity"; +import { LevelMapper } from "./level.mapper"; + +@Injectable() +export class LevelService extends NamedEntityService { + constructor(prisma: PrismaService, levelMapper: LevelMapper) { + super(prisma, levelMapper, prisma.level, "Level"); + } +} diff --git a/packages/core/src/modules/job-experience/role/role.dataloader.ts b/packages/core/src/modules/job-experience/role/role.dataloader.ts new file mode 100644 index 0000000..47d8746 --- /dev/null +++ b/packages/core/src/modules/job-experience/role/role.dataloader.ts @@ -0,0 +1,20 @@ +import { BaseDataLoaderService } from "../../../shared"; +import { Injectable, Scope } from "@nestjs/common"; +import { Role } from "./role.entity"; +import { RoleService } from "./role.service"; + +@Injectable({ scope: Scope.REQUEST }) +export class RoleDataLoaderService extends BaseDataLoaderService { + constructor(readonly roleService: RoleService) { + super(async (ids: readonly string[]) => { + const roles = await roleService.findMany({ id: [...ids] }); + + const roleMap = new Map(); + for (const role of roles) { + roleMap.set(role.id, role); + } + + return ids.map((id) => roleMap.get(id) ?? null); + }); + } +} diff --git a/packages/core/src/modules/job-experience/role/role.dto.ts b/packages/core/src/modules/job-experience/role/role.dto.ts new file mode 100644 index 0000000..b0f040f --- /dev/null +++ b/packages/core/src/modules/job-experience/role/role.dto.ts @@ -0,0 +1,9 @@ +export interface CreateRoleDto { + name: string; + description?: string; +} + +export interface UpdateRoleDto { + name?: string; + description?: string; +} diff --git a/packages/core/src/modules/job-experience/role/role.entity.ts b/packages/core/src/modules/job-experience/role/role.entity.ts new file mode 100644 index 0000000..72ff32e --- /dev/null +++ b/packages/core/src/modules/job-experience/role/role.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "../../../shared"; + +export class Role extends NamedEntity {} diff --git a/packages/core/src/modules/job-experience/role/role.factory.ts b/packages/core/src/modules/job-experience/role/role.factory.ts new file mode 100644 index 0000000..40072b2 --- /dev/null +++ b/packages/core/src/modules/job-experience/role/role.factory.ts @@ -0,0 +1,23 @@ +import { ClockService, Factory, UuidFactoryService } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { CreateRoleDto } from "./role.dto"; +import { Role } from "./role.entity"; + +@Injectable() +export class RoleFactory implements Factory { + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(dto: CreateRoleDto): Role { + const now = this.clock.now(); + return new Role( + this.uuidFactory.generate(), + dto.name, + now, + now, + dto.description, + ); + } +} diff --git a/packages/core/src/modules/job-experience/role/role.mapper.ts b/packages/core/src/modules/job-experience/role/role.mapper.ts new file mode 100644 index 0000000..2eae1a3 --- /dev/null +++ b/packages/core/src/modules/job-experience/role/role.mapper.ts @@ -0,0 +1,23 @@ +import { createNamedEntityMapper } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { Role as PrismaRole } from "@prisma/client"; +import { Role } from "./role.entity"; + +/** + * Mapper service for converting between Prisma Role entities and domain Role entities + */ +@Injectable() +export class RoleMapper { + private mapper = createNamedEntityMapper(Role); + + toDomain(prismaRole: null): null; + toDomain(prismaRole: PrismaRole): Role; + toDomain(prismaRole: PrismaRole | null): Role | null; + toDomain(prismaRole: PrismaRole | null): Role | null { + return this.mapper.toDomain(prismaRole); + } + + mapToDomain(prismaRoles: PrismaRole[]): Role[] { + return this.mapper.mapToDomain(prismaRoles); + } +} diff --git a/packages/core/src/modules/job-experience/role/role.module.ts b/packages/core/src/modules/job-experience/role/role.module.ts new file mode 100644 index 0000000..55182e3 --- /dev/null +++ b/packages/core/src/modules/job-experience/role/role.module.ts @@ -0,0 +1,28 @@ +import { AuthorizationModule } from "../../auth"; +import { BaseModule } from "../../../shared"; +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "../../authentication/authentication.module"; +import { RoleService } from "./role.service"; +import { RoleDataLoaderService } from "./role.dataloader"; +import { RoleFactory } from "./role.factory"; +import { RoleMapper } from "./role.mapper"; +import { RolePolicy } from "./role.policy"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + ], + providers: [ + RoleService, + RoleMapper, + RoleFactory, + RolePolicy, + RoleDataLoaderService, + ], + exports: [RoleService, RoleMapper, RoleDataLoaderService], +}) +export class RoleModule {} diff --git a/packages/core/src/modules/job-experience/role/role.policy.ts b/packages/core/src/modules/job-experience/role/role.policy.ts new file mode 100644 index 0000000..44a914a --- /dev/null +++ b/packages/core/src/modules/job-experience/role/role.policy.ts @@ -0,0 +1,11 @@ +import { Policy, PublicResourcePolicy } from "../../auth"; +import { Injectable } from "@nestjs/common"; +import { Role } from "./role.entity"; + +@Injectable() +@Policy(Role) +export class RolePolicy extends PublicResourcePolicy { + override create(): boolean { + return true; + } +} diff --git a/packages/core/src/modules/job-experience/role/role.service.ts b/packages/core/src/modules/job-experience/role/role.service.ts new file mode 100644 index 0000000..673c30c --- /dev/null +++ b/packages/core/src/modules/job-experience/role/role.service.ts @@ -0,0 +1,12 @@ +import { NamedEntityService } from "../../../shared"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { Role } from "./role.entity"; +import { RoleMapper } from "./role.mapper"; + +@Injectable() +export class RoleService extends NamedEntityService { + constructor(prisma: PrismaService, roleMapper: RoleMapper) { + super(prisma, roleMapper, prisma.role, "Role"); + } +} diff --git a/packages/core/src/modules/job-experience/seed/job-experience.seed.ts b/packages/core/src/modules/job-experience/seed/job-experience.seed.ts new file mode 100644 index 0000000..2e2a0c3 --- /dev/null +++ b/packages/core/src/modules/job-experience/seed/job-experience.seed.ts @@ -0,0 +1,94 @@ +import { PrismaService } from "../../database"; +import { faker } from "@faker-js/faker"; +import { Injectable, Logger } from "@nestjs/common"; +import { Seeder } from "../../database/seed/seed.service"; +import { Seeder as SeederDecorator } from "../../database/seed/seeder.decorator"; +import { ReferenceDataSeedService } from "./reference-data.seed"; + +@Injectable() +@SeederDecorator({ name: "Job Experiences", dependsOn: ["Users", "Reference Data"] }) +export class JobExperienceSeedService implements Seeder { + private readonly logger = new Logger(JobExperienceSeedService.name); + + constructor( + readonly _prisma: PrismaService, + private readonly refs: ReferenceDataSeedService, + ) {} + + async seed(prisma: PrismaService): Promise { + this.logger.log("Seeding job experiences..."); + + // First ensure reference data exists + const [skills, companies, roles, levels] = await Promise.all([ + this.refs.ensureSkills(prisma), + this.refs.ensureCompanies(prisma), + this.refs.ensureRoles(prisma), + this.refs.ensureLevels(prisma), + ]); + + // Get test user by email from credentials + const credentials = await prisma["credentials"].findUnique({ + where: { email: "test@test.test" }, + include: { user: true }, + }); + const testUser = credentials?.user ?? null; + + if (!testUser) { + this.logger.warn("Test user not found, skipping job experience seeding"); + return; + } + + // Get or create profile for the test user + const existingProfile = await prisma.profile.findFirst({ + where: { userId: testUser.id }, + }); + const profile = existingProfile ?? await prisma.profile.create({ + data: { userId: testUser.id, name: testUser.name }, + }); + + // Clear existing job experiences for this profile only + this.logger.log("Clearing existing job experiences for test user..."); + await prisma.userJobExperience.deleteMany({ + where: { profileId: profile.id }, + }); + + // Create job experiences for the test user + const numExperiences = faker.number.int({ min: 2, max: 5 }); + + await Promise.all( + Array.from({ length: numExperiences }, async () => { + const startDate = faker.date.past({ years: 8 }); + const endDate = faker.datatype.boolean() + ? faker.date.between({ from: startDate, to: new Date() }) + : null; + + const company = faker.helpers.arrayElement(companies); + const role = faker.helpers.arrayElement(roles); + const level = faker.helpers.arrayElement(levels); + + // Select 3-8 random skills for this job experience + const selectedSkills = faker.helpers.arrayElements(skills, { + min: 3, + max: 8, + }); + + return prisma.userJobExperience.create({ + data: { + profileId: profile.id, + companyId: company.id, + roleId: role.id, + levelId: level.id, + startDate, + endDate, + description: faker.lorem.paragraph(), + skills: { + connect: selectedSkills.map((skill) => ({ id: skill.id })), + }, + }, + }); + }), + ); + + this.logger.log(`Created ${numExperiences} job experiences for test user`); + } +} diff --git a/packages/core/src/modules/job-experience/seed/reference-data.seed.ts b/packages/core/src/modules/job-experience/seed/reference-data.seed.ts new file mode 100644 index 0000000..6cb0e53 --- /dev/null +++ b/packages/core/src/modules/job-experience/seed/reference-data.seed.ts @@ -0,0 +1,196 @@ +import { PrismaService } from "../../database"; +import { faker } from "@faker-js/faker"; +import { Injectable, Logger } from "@nestjs/common"; +import { Seeder } from "../../database/seed/seed.service"; +import { Seeder as SeederDecorator } from "../../database/seed/seeder.decorator"; + +@Injectable() +@SeederDecorator("Reference Data") +export class ReferenceDataSeedService implements Seeder { + private readonly logger = new Logger(ReferenceDataSeedService.name); + + constructor(private readonly prisma: PrismaService) {} + + async seed(prisma: PrismaService): Promise { + this.logger.log("Seeding reference data..."); + await Promise.all([ + this.ensureSkills(prisma), + this.ensureCompanies(prisma), + this.ensureRoles(prisma), + this.ensureLevels(prisma), + this.ensureJobTypes(prisma), + this.ensureApplicationStatuses(prisma), + this.ensureRateTypes(prisma), + this.ensureContractTypes(prisma), + ]); + } + + async ensureSkills( + prisma: PrismaService = this.prisma, + ): Promise> { + const skills = ["React", "TypeScript", "Node.js", "GraphQL", "PostgreSQL"]; + + return Promise.all( + skills.map(async (name) => { + const existing = await prisma["skill"].findFirst({ + where: { name }, + }); + if (existing) { + return { id: existing.id }; + } + const s = await prisma["skill"].create({ + data: { name, description: faker.lorem.sentence() }, + }); + return { id: s.id }; + }), + ); + } + + async ensureCompanies( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = ["Google", "Microsoft", "Amazon", "Netflix"]; + + return Promise.all( + names.map(async (name) => { + const existing = await prisma["company"].findFirst({ + where: { name }, + }); + if (existing) { + return { id: existing.id }; + } + const c = await prisma["company"].create({ + data: { + name, + description: faker.company.catchPhrase(), + website: faker.internet.url(), + }, + }); + return { id: c.id }; + }), + ); + } + + async ensureRoles( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = [ + "Software Engineer", + "Senior Software Engineer", + "Engineering Manager", + ]; + + return Promise.all( + names.map(async (name) => { + const existing = await prisma["role"].findFirst({ where: { name } }); + if (existing) { + return { id: existing.id }; + } + const r = await prisma["role"].create({ + data: { name, description: faker.lorem.sentence() }, + }); + return { id: r.id }; + }), + ); + } + + async ensureLevels( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = ["Junior", "Mid-level", "Senior"]; + + return Promise.all( + names.map(async (name) => { + const existing = await prisma["level"].findFirst({ + where: { name }, + }); + if (existing) { + return { id: existing.id }; + } + const l = await prisma["level"].create({ + data: { name, description: faker.lorem.sentence() }, + }); + return { id: l.id }; + }), + ); + } + + async ensureJobTypes( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = [ + "Full-time", + "Part-time", + "Freelance", + "Contract", + "Internship", + "Temporary", + ]; + + return Promise.all( + names.map(async (name) => { + const existing = await prisma["jobType"].findFirst({ + where: { name }, + }); + if (existing) { + return { id: existing.id }; + } + const jt = await prisma["jobType"].create({ + data: { name, description: faker.lorem.sentence() }, + }); + return { id: jt.id }; + }), + ); + } + + async ensureRateTypes( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = ["HOUR", "AGREEMENT", "FIXED", "DAY"]; + + return Promise.all( + names.map(async (name) => { + const existing = await prisma.rateType.findFirst({ where: { name } }); + if (existing) return { id: existing.id }; + const rt = await prisma.rateType.create({ data: { name } }); + return { id: rt.id }; + }), + ); + } + + async ensureContractTypes( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = ["FREELANCE", "FIXED_TERM", "PERMANENT", "INTERNSHIP"]; + + return Promise.all( + names.map(async (name) => { + const existing = await prisma.contractType.findFirst({ where: { name } }); + if (existing) return { id: existing.id }; + const ct = await prisma.contractType.create({ data: { name } }); + return { id: ct.id }; + }), + ); + } + + async ensureApplicationStatuses( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = ["Pending", "Accepted", "Rejected", "Withdrawn"]; + + return Promise.all( + names.map(async (name) => { + const existing = await prisma["applicationStatus"].findFirst({ + where: { name }, + }); + if (existing) { + return { id: existing.id }; + } + const status = await prisma["applicationStatus"].create({ + data: { name, description: faker.lorem.sentence() }, + }); + return { id: status.id }; + }), + ); + } +} diff --git a/packages/core/src/modules/job-experience/skill/skill.dto.ts b/packages/core/src/modules/job-experience/skill/skill.dto.ts new file mode 100644 index 0000000..74b357d --- /dev/null +++ b/packages/core/src/modules/job-experience/skill/skill.dto.ts @@ -0,0 +1,9 @@ +export interface CreateSkillDto { + name: string; + description?: string; +} + +export interface UpdateSkillDto { + name?: string; + description?: string; +} diff --git a/packages/core/src/modules/job-experience/skill/skill.entity.ts b/packages/core/src/modules/job-experience/skill/skill.entity.ts new file mode 100644 index 0000000..1d72f80 --- /dev/null +++ b/packages/core/src/modules/job-experience/skill/skill.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "../../../shared"; + +export class Skill extends NamedEntity {} diff --git a/packages/core/src/modules/job-experience/skill/skill.factory.ts b/packages/core/src/modules/job-experience/skill/skill.factory.ts new file mode 100644 index 0000000..8118a24 --- /dev/null +++ b/packages/core/src/modules/job-experience/skill/skill.factory.ts @@ -0,0 +1,23 @@ +import { ClockService, Factory, UuidFactoryService } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { CreateSkillDto } from "./skill.dto"; +import { Skill } from "./skill.entity"; + +@Injectable() +export class SkillFactory implements Factory { + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(dto: CreateSkillDto): Skill { + const now = this.clock.now(); + return new Skill( + this.uuidFactory.generate(), + dto.name, + now, + now, + dto.description, + ); + } +} diff --git a/packages/core/src/modules/job-experience/skill/skill.mapper.ts b/packages/core/src/modules/job-experience/skill/skill.mapper.ts new file mode 100644 index 0000000..3adaa5b --- /dev/null +++ b/packages/core/src/modules/job-experience/skill/skill.mapper.ts @@ -0,0 +1,23 @@ +import { createNamedEntityMapper } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { Skill as PrismaSkill } from "@prisma/client"; +import { Skill } from "./skill.entity"; + +/** + * Mapper service for converting between Prisma Skill entities and domain Skill entities + */ +@Injectable() +export class SkillMapper { + private mapper = createNamedEntityMapper(Skill); + + toDomain(prismaSkill: null): null; + toDomain(prismaSkill: PrismaSkill): Skill; + toDomain(prismaSkill: PrismaSkill | null): Skill | null; + toDomain(prismaSkill: PrismaSkill | null): Skill | null { + return this.mapper.toDomain(prismaSkill); + } + + mapToDomain(prismaSkills: PrismaSkill[]): Skill[] { + return this.mapper.mapToDomain(prismaSkills); + } +} diff --git a/packages/core/src/modules/job-experience/skill/skill.module.ts b/packages/core/src/modules/job-experience/skill/skill.module.ts new file mode 100644 index 0000000..143d002 --- /dev/null +++ b/packages/core/src/modules/job-experience/skill/skill.module.ts @@ -0,0 +1,30 @@ +import { AuthorizationModule } from "../../auth"; +import { BaseModule } from "../../../shared"; +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "../../authentication/authentication.module"; +import { SkillService } from "./skill.service"; +import { JobExperienceSeedService } from "../seed/job-experience.seed"; +import { ReferenceDataSeedService } from "../seed/reference-data.seed"; +import { SkillFactory } from "./skill.factory"; +import { SkillMapper } from "./skill.mapper"; +import { SkillPolicy } from "./skill.policy"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + ], + providers: [ + SkillService, + SkillMapper, + SkillFactory, + SkillPolicy, + ReferenceDataSeedService, + JobExperienceSeedService, + ], + exports: [SkillService, SkillMapper], +}) +export class SkillModule {} diff --git a/packages/core/src/modules/job-experience/skill/skill.policy.ts b/packages/core/src/modules/job-experience/skill/skill.policy.ts new file mode 100644 index 0000000..c6f9ff5 --- /dev/null +++ b/packages/core/src/modules/job-experience/skill/skill.policy.ts @@ -0,0 +1,11 @@ +import { Policy, PublicResourcePolicy } from "../../auth"; +import { Injectable } from "@nestjs/common"; +import { Skill } from "./skill.entity"; + +@Injectable() +@Policy(Skill) +export class SkillPolicy extends PublicResourcePolicy { + override create(): boolean { + return true; + } +} diff --git a/packages/core/src/modules/job-experience/skill/skill.service.ts b/packages/core/src/modules/job-experience/skill/skill.service.ts new file mode 100644 index 0000000..7973c5e --- /dev/null +++ b/packages/core/src/modules/job-experience/skill/skill.service.ts @@ -0,0 +1,40 @@ +import { type NamedEntityFilters, NamedEntityService } from "../../../shared"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { Skill } from "./skill.entity"; +import { SkillMapper } from "./skill.mapper"; + +type SkillFilters = NamedEntityFilters & { + vacancyId?: string | undefined; +}; + +type SkillWhereInput = { + name?: { contains: string; mode: "insensitive" }; + vacancies?: { some: { id: string } }; + id?: { in: string[] } | string; +}; + +@Injectable() +export class SkillService extends NamedEntityService< + Skill, + SkillFilters, + SkillWhereInput +> { + constructor(prisma: PrismaService, skillMapper: SkillMapper) { + super(prisma, skillMapper, prisma.skill, "Skill"); + } + + protected override buildWhere(filters: SkillFilters = {}): SkillWhereInput { + const where = super.buildWhere(filters) as SkillWhereInput; + + if (filters.vacancyId) { + where.vacancies = { + some: { + id: filters.vacancyId, + }, + }; + } + + return where; + } +} diff --git a/packages/system/src/mail/index.ts b/packages/core/src/modules/mail/index.ts similarity index 100% rename from packages/system/src/mail/index.ts rename to packages/core/src/modules/mail/index.ts diff --git a/packages/system/src/mail/mail.module.ts b/packages/core/src/modules/mail/mail.module.ts similarity index 100% rename from packages/system/src/mail/mail.module.ts rename to packages/core/src/modules/mail/mail.module.ts diff --git a/packages/system/src/mail/mail.service.interface.ts b/packages/core/src/modules/mail/mail.service.interface.ts similarity index 100% rename from packages/system/src/mail/mail.service.interface.ts rename to packages/core/src/modules/mail/mail.service.interface.ts diff --git a/packages/system/src/mail/providers/console-mail.service.ts b/packages/core/src/modules/mail/providers/console-mail.service.ts similarity index 100% rename from packages/system/src/mail/providers/console-mail.service.ts rename to packages/core/src/modules/mail/providers/console-mail.service.ts diff --git a/packages/system/src/mail/providers/example-mail.service.ts b/packages/core/src/modules/mail/providers/example-mail.service.ts similarity index 100% rename from packages/system/src/mail/providers/example-mail.service.ts rename to packages/core/src/modules/mail/providers/example-mail.service.ts diff --git a/packages/system/src/mail/resend/index.ts b/packages/core/src/modules/mail/resend/index.ts similarity index 100% rename from packages/system/src/mail/resend/index.ts rename to packages/core/src/modules/mail/resend/index.ts diff --git a/packages/system/src/mail/resend/resend-mail.service.ts b/packages/core/src/modules/mail/resend/resend-mail.service.ts similarity index 100% rename from packages/system/src/mail/resend/resend-mail.service.ts rename to packages/core/src/modules/mail/resend/resend-mail.service.ts diff --git a/packages/system/src/mail/resend/resend.module.ts b/packages/core/src/modules/mail/resend/resend.module.ts similarity index 100% rename from packages/system/src/mail/resend/resend.module.ts rename to packages/core/src/modules/mail/resend/resend.module.ts diff --git a/packages/system/src/mail/template/handlebars-template.service.ts b/packages/core/src/modules/mail/template/handlebars-template.service.ts similarity index 100% rename from packages/system/src/mail/template/handlebars-template.service.ts rename to packages/core/src/modules/mail/template/handlebars-template.service.ts diff --git a/packages/system/src/mail/template/index.ts b/packages/core/src/modules/mail/template/index.ts similarity index 100% rename from packages/system/src/mail/template/index.ts rename to packages/core/src/modules/mail/template/index.ts diff --git a/packages/system/src/mail/template/template-registry.service.ts b/packages/core/src/modules/mail/template/template-registry.service.ts similarity index 100% rename from packages/system/src/mail/template/template-registry.service.ts rename to packages/core/src/modules/mail/template/template-registry.service.ts diff --git a/packages/system/src/mail/template/template.module.ts b/packages/core/src/modules/mail/template/template.module.ts similarity index 100% rename from packages/system/src/mail/template/template.module.ts rename to packages/core/src/modules/mail/template/template.module.ts diff --git a/packages/core/src/modules/messenger/logger.provider.ts b/packages/core/src/modules/messenger/logger.provider.ts new file mode 100644 index 0000000..ad00320 --- /dev/null +++ b/packages/core/src/modules/messenger/logger.provider.ts @@ -0,0 +1,18 @@ +import { Logger as NestLogger } from "@nestjs/common"; +import type { Logger } from "@riotbyte-com/project-q-core"; + +export class NestProjectQLogger implements Logger { + private readonly logger = new NestLogger("ProjectQ"); + + info(message: string): void { + this.logger.log(message); + } + + error(message: string): void { + this.logger.error(message); + } + + debug(message: string): void { + this.logger.debug(message); + } +} diff --git a/packages/core/src/modules/messenger/messages/render-pdf.message.ts b/packages/core/src/modules/messenger/messages/render-pdf.message.ts new file mode 100644 index 0000000..e39cf5d --- /dev/null +++ b/packages/core/src/modules/messenger/messages/render-pdf.message.ts @@ -0,0 +1,11 @@ +import { defineZodMessage } from "@riotbyte-com/project-q-core"; +import { z } from "zod/v4"; + +export const RenderPdfMessage = defineZodMessage( + "render-pdf", + z.object({ + cvId: z.string(), + html: z.string(), + requestedBy: z.string(), + }), +); diff --git a/packages/core/src/modules/messenger/messenger.module.ts b/packages/core/src/modules/messenger/messenger.module.ts new file mode 100644 index 0000000..11851a7 --- /dev/null +++ b/packages/core/src/modules/messenger/messenger.module.ts @@ -0,0 +1,23 @@ +import { Module } from "@nestjs/common"; +import { LoggerProvider, MessengerModule } from "@riotbyte-com/project-q-nestjs"; +import { NestProjectQLogger } from "./logger.provider"; +import { PrismaTransportModule } from "./prisma-transport.module"; + +@Module({ + imports: [ + PrismaTransportModule, + MessengerModule.forRoot({ + transports: { + async: { dsn: "prisma://?queue=default", retry: true }, + }, + routing: new Map([["render-pdf", "async"]]), + }), + ], + providers: [ + { + provide: LoggerProvider, + useClass: NestProjectQLogger, + }, + ], +}) +export class ProjectQMessengerModule {} diff --git a/packages/core/src/modules/messenger/prisma-transport.module.ts b/packages/core/src/modules/messenger/prisma-transport.module.ts new file mode 100644 index 0000000..76629a4 --- /dev/null +++ b/packages/core/src/modules/messenger/prisma-transport.module.ts @@ -0,0 +1,13 @@ +import { Module } from "@nestjs/common"; +import { DatabaseModule, PrismaService } from "../database"; +import { PrismaClientToken, PrismaTransportFactory } from "@riotbyte-com/project-q-prisma"; + +@Module({ + imports: [DatabaseModule], + providers: [ + PrismaTransportFactory, + { provide: PrismaClientToken, useExisting: PrismaService }, + ], + exports: [PrismaTransportFactory], +}) +export class PrismaTransportModule {} diff --git a/packages/core/src/modules/onboarding/onboarding-step.decorator.ts b/packages/core/src/modules/onboarding/onboarding-step.decorator.ts new file mode 100644 index 0000000..1edc9e7 --- /dev/null +++ b/packages/core/src/modules/onboarding/onboarding-step.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from "@nestjs/common"; + +export const ONBOARDING_STEP_METADATA_KEY = "onboarding_step"; + +export const OnboardingStep = () => + SetMetadata(ONBOARDING_STEP_METADATA_KEY, true); diff --git a/packages/core/src/modules/onboarding/onboarding-step.interface.ts b/packages/core/src/modules/onboarding/onboarding-step.interface.ts new file mode 100644 index 0000000..c74a285 --- /dev/null +++ b/packages/core/src/modules/onboarding/onboarding-step.interface.ts @@ -0,0 +1,14 @@ +import type { User } from "../auth"; + +export enum OnboardingStepStatus { + NOT_STARTED = "NOT_STARTED", + IN_PROGRESS = "IN_PROGRESS", + COMPLETE = "COMPLETE", +} + +export interface OnboardingStepDefinition { + name: string; + order: number; + dependsOn: string[]; + computeStatus(user: User): Promise; +} diff --git a/packages/core/src/modules/onboarding/onboarding.module.ts b/packages/core/src/modules/onboarding/onboarding.module.ts new file mode 100644 index 0000000..daefd3d --- /dev/null +++ b/packages/core/src/modules/onboarding/onboarding.module.ts @@ -0,0 +1,11 @@ +import { DatabaseModule } from "../database"; +import { Module } from "@nestjs/common"; +import { DiscoveryModule } from "@nestjs/core"; +import { OnboardingService } from "./onboarding.service"; + +@Module({ + imports: [DiscoveryModule, DatabaseModule], + providers: [OnboardingService], + exports: [OnboardingService], +}) +export class OnboardingModule {} diff --git a/packages/core/src/modules/onboarding/onboarding.service.ts b/packages/core/src/modules/onboarding/onboarding.service.ts new file mode 100644 index 0000000..4ebd52d --- /dev/null +++ b/packages/core/src/modules/onboarding/onboarding.service.ts @@ -0,0 +1,101 @@ +import type { User } from "../auth"; +import { PrismaService } from "../database"; +import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; +import { DiscoveryService } from "@nestjs/core"; +import { ONBOARDING_STEP_METADATA_KEY } from "./onboarding-step.decorator"; +import type { + OnboardingStepDefinition, + OnboardingStepStatus, +} from "./onboarding-step.interface"; + +export interface OnboardingStepResult { + name: string; + status: OnboardingStepStatus; + dependsOn: string[]; + blockedBy: string[]; +} + +@Injectable() +export class OnboardingService implements OnModuleInit { + private readonly logger = new Logger(OnboardingService.name); + private steps: OnboardingStepDefinition[] = []; + + constructor( + private readonly discoveryService: DiscoveryService, + private readonly prisma: PrismaService, + ) {} + + async onModuleInit(): Promise { + const providers = this.discoveryService.getProviders(); + + for (const wrapper of providers) { + if (!(wrapper.isDependencyTreeStatic() && wrapper.instance)) continue; + + const metadata = Reflect.getMetadata( + ONBOARDING_STEP_METADATA_KEY, + wrapper.instance.constructor, + ); + + if (!metadata) continue; + + const instance = wrapper.instance as OnboardingStepDefinition; + + if ( + typeof instance.name !== "string" || + typeof instance.order !== "number" || + !Array.isArray(instance.dependsOn) || + typeof instance.computeStatus !== "function" + ) { + this.logger.warn( + `Provider ${wrapper.instance.constructor.name} has @OnboardingStep() but doesn't implement OnboardingStepDefinition`, + ); + continue; + } + + this.steps.push(instance); + this.logger.log(`Registered onboarding step: ${instance.name}`); + } + + this.steps.sort((a, b) => a.order - b.order); + } + + async resetOnboarding(user: User): Promise { + await this.prisma.$transaction([ + this.prisma.education.deleteMany({ + where: { profile: { userId: user.id } }, + }), + this.prisma.userJobExperience.deleteMany({ + where: { profile: { userId: user.id } }, + }), + this.prisma.importJob.deleteMany({ + where: { userFile: { profile: { userId: user.id } } }, + }), + this.prisma.userFile.deleteMany({ + where: { profile: { userId: user.id } }, + }), + this.prisma.profile.deleteMany({ where: { userId: user.id } }), + this.prisma.userAiProvider.deleteMany({ where: { userId: user.id } }), + this.prisma.userAiSettings.deleteMany({ where: { userId: user.id } }), + ]); + + this.logger.log(`Onboarding reset for user ${user.id}`); + return true; + } + + async getStatus(user: User): Promise { + const statuses = await Promise.all( + this.steps.map(async (step) => ({ + name: step.name, + status: await step.computeStatus(user), + dependsOn: step.dependsOn, + })), + ); + + const statusMap = new Map(statuses.map((s) => [s.name, s.status])); + + return statuses.map((s) => ({ + ...s, + blockedBy: s.dependsOn.filter((dep) => statusMap.get(dep) !== "COMPLETE"), + })); + } +} diff --git a/packages/core/src/modules/organization/organization-role.entity.ts b/packages/core/src/modules/organization/organization-role.entity.ts new file mode 100644 index 0000000..104d663 --- /dev/null +++ b/packages/core/src/modules/organization/organization-role.entity.ts @@ -0,0 +1,55 @@ +import { BaseEntity } from "../../shared"; +import { Field, ID, ObjectType } from "@nestjs/graphql"; + +@ObjectType() +export class OrganizationRole extends BaseEntity { + @Field(() => ID) + declare id: string; + + @Field(() => String) + declare name: string; + + @Field(() => String, { nullable: true }) + declare description: string | null; + + @Field(() => String, { nullable: true }) + declare color: string | null; + + @Field(() => Date) + declare createdAt: Date; + + @Field(() => Date) + declare updatedAt: Date; + + constructor( + id: string, + name: string, + createdAt: Date, + updatedAt: Date, + description: string | null = null, + color: string | null = null, + ) { + super(id, createdAt, updatedAt); + this.name = name; + this.description = description; + this.color = color; + } + + static fromDomain(domainRole: { + id: string; + name: string; + description?: string | null; + color?: string | null; + createdAt: Date; + updatedAt: Date; + }): OrganizationRole { + return new OrganizationRole( + domainRole.id, + domainRole.name, + domainRole.createdAt, + domainRole.updatedAt, + domainRole.description, + domainRole.color, + ); + } +} diff --git a/packages/core/src/modules/organization/organization-role.mapper.ts b/packages/core/src/modules/organization/organization-role.mapper.ts new file mode 100644 index 0000000..2569ad4 --- /dev/null +++ b/packages/core/src/modules/organization/organization-role.mapper.ts @@ -0,0 +1,30 @@ +import type { BaseMapper } from "../../shared"; +import { Injectable } from "@nestjs/common"; +import type { OrganizationRole as PrismaOrganizationRole } from "@prisma/client"; +import { OrganizationRole } from "./organization-role.entity"; + +@Injectable() +export class OrganizationRoleMapper + implements BaseMapper +{ + toDomain(prismaRole: null): null; + toDomain(prismaRole: PrismaOrganizationRole): OrganizationRole; + toDomain(prismaRole: PrismaOrganizationRole | null): OrganizationRole | null; + toDomain(prismaRole: PrismaOrganizationRole | null): OrganizationRole | null { + if (prismaRole === null) { + return null; + } + return new OrganizationRole( + prismaRole.id, + prismaRole.name, + prismaRole.createdAt, + prismaRole.updatedAt, + prismaRole.description, + prismaRole.color, + ); + } + + mapToDomain(prismaRoles: PrismaOrganizationRole[]): OrganizationRole[] { + return prismaRoles.map((r) => this.toDomain(r)); + } +} diff --git a/packages/core/src/modules/organization/organization-role.policy.ts b/packages/core/src/modules/organization/organization-role.policy.ts new file mode 100644 index 0000000..5bf6bce --- /dev/null +++ b/packages/core/src/modules/organization/organization-role.policy.ts @@ -0,0 +1,7 @@ +import { Policy, PublicResourcePolicy } from "../auth"; +import { Injectable } from "@nestjs/common"; +import { OrganizationRole } from "./organization-role.entity"; + +@Injectable() +@Policy(OrganizationRole) +export class OrganizationRolePolicy extends PublicResourcePolicy {} diff --git a/packages/core/src/modules/organization/organization-role.service.ts b/packages/core/src/modules/organization/organization-role.service.ts new file mode 100644 index 0000000..729726e --- /dev/null +++ b/packages/core/src/modules/organization/organization-role.service.ts @@ -0,0 +1,77 @@ +import { notFound } from "../auth"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import { OrganizationRole } from "./organization-role.entity"; +import { OrganizationRoleMapper } from "./organization-role.mapper"; + +@Injectable() +export class OrganizationRoleService { + constructor( + private readonly prisma: PrismaService, + private readonly organizationRoleMapper: OrganizationRoleMapper, + ) {} + + async findById(id: string): Promise { + const role = await this.prisma.organizationRole.findUnique({ + where: { id }, + }); + + return this.organizationRoleMapper.toDomain(role); + } + + async findByIdOrFail(id: string): Promise { + const role = await this.findById(id); + return role ?? notFound("Organization role", "id", id); + } + + async findAll(): Promise { + const roles = await this.prisma.organizationRole.findMany({ + orderBy: { name: "asc" }, + }); + return this.organizationRoleMapper.mapToDomain(roles); + } + + async create(data: { + name: string; + description?: string; + color?: string; + }): Promise { + const role = await this.prisma.organizationRole.create({ + data: { + name: data.name, + description: data.description ?? null, + color: data.color ?? "#6366f1", + }, + }); + + return this.organizationRoleMapper.toDomain(role); + } + + async update( + id: string, + data: { + name?: string; + description?: string; + color?: string; + }, + ): Promise { + const role = await this.prisma.organizationRole.update({ + where: { id }, + data: { + ...(data.name !== undefined && { name: data.name }), + ...(data.description !== undefined && { + description: data.description, + }), + ...(data.color !== undefined && { color: data.color }), + }, + }); + + return this.organizationRoleMapper.toDomain(role); + } + + async delete(id: string): Promise { + await this.prisma.organizationRole.delete({ + where: { id }, + }); + } +} diff --git a/packages/core/src/modules/organization/organization.dto.ts b/packages/core/src/modules/organization/organization.dto.ts new file mode 100644 index 0000000..da5837d --- /dev/null +++ b/packages/core/src/modules/organization/organization.dto.ts @@ -0,0 +1,9 @@ +export interface CreateOrganizationDto { + name: string; + description?: string; +} + +export interface UpdateOrganizationDto { + name?: string; + description?: string; +} diff --git a/packages/core/src/modules/organization/organization.entity.ts b/packages/core/src/modules/organization/organization.entity.ts new file mode 100644 index 0000000..11bf242 --- /dev/null +++ b/packages/core/src/modules/organization/organization.entity.ts @@ -0,0 +1,48 @@ +import { BaseEntity } from "../../shared"; +import { Field, ID, ObjectType } from "@nestjs/graphql"; + +@ObjectType() +export class Organization extends BaseEntity { + @Field(() => ID) + declare id: string; + + @Field(() => String) + declare name: string; + + @Field(() => String, { nullable: true }) + declare description: string | null; + + @Field(() => Date) + declare createdAt: Date; + + @Field(() => Date) + declare updatedAt: Date; + + constructor( + id: string, + name: string, + createdAt: Date, + updatedAt: Date, + description: string | null = null, + ) { + super(id, createdAt, updatedAt); + this.name = name; + this.description = description; + } + + static fromDomain(domainOrg: { + id: string; + name: string; + description?: string | null; + createdAt: Date; + updatedAt: Date; + }): Organization { + return new Organization( + domainOrg.id, + domainOrg.name, + domainOrg.createdAt, + domainOrg.updatedAt, + domainOrg.description, + ); + } +} diff --git a/packages/core/src/modules/organization/organization.factory.ts b/packages/core/src/modules/organization/organization.factory.ts new file mode 100644 index 0000000..309042d --- /dev/null +++ b/packages/core/src/modules/organization/organization.factory.ts @@ -0,0 +1,25 @@ +import { ClockService, Factory, UuidFactoryService } from "../../shared"; +import { Injectable } from "@nestjs/common"; +import type { CreateOrganizationDto } from "./organization.dto"; +import { Organization } from "./organization.entity"; + +@Injectable() +export class OrganizationFactory + implements Factory +{ + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(dto: CreateOrganizationDto): Organization { + const now = this.clock.now(); + return new Organization( + this.uuidFactory.generate(), + dto.name, + now, + now, + dto.description ?? null, + ); + } +} diff --git a/packages/core/src/modules/organization/organization.mapper.ts b/packages/core/src/modules/organization/organization.mapper.ts new file mode 100644 index 0000000..7313681 --- /dev/null +++ b/packages/core/src/modules/organization/organization.mapper.ts @@ -0,0 +1,29 @@ +import type { BaseMapper } from "../../shared"; +import { Injectable } from "@nestjs/common"; +import type { Organization as PrismaOrganization } from "@prisma/client"; +import { Organization } from "./organization.entity"; + +@Injectable() +export class OrganizationMapper + implements BaseMapper +{ + toDomain(prismaOrganization: null): null; + toDomain(prismaOrganization: PrismaOrganization): Organization; + toDomain(prismaOrganization: PrismaOrganization | null): Organization | null; + toDomain(prismaOrganization: PrismaOrganization | null): Organization | null { + if (prismaOrganization === null) { + return null; + } + return new Organization( + prismaOrganization.id, + prismaOrganization.name, + prismaOrganization.createdAt, + prismaOrganization.updatedAt, + prismaOrganization.description, + ); + } + + mapToDomain(prismaOrganizations: PrismaOrganization[]): Organization[] { + return prismaOrganizations.map((o) => this.toDomain(o)); + } +} diff --git a/packages/core/src/modules/organization/organization.module.ts b/packages/core/src/modules/organization/organization.module.ts new file mode 100644 index 0000000..997dd57 --- /dev/null +++ b/packages/core/src/modules/organization/organization.module.ts @@ -0,0 +1,41 @@ +import { AuthorizationModule } from "../auth"; +import { BaseModule } from "../../shared"; +import { DatabaseModule } from "../database"; +import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "../authentication/authentication.module"; +import { OrganizationFactory } from "./organization.factory"; +import { OrganizationMapper } from "./organization.mapper"; +import { OrganizationPolicy } from "./organization.policy"; +import { OrganizationService } from "./organization.service"; +import { OrganizationRoleMapper } from "./organization-role.mapper"; +import { OrganizationRolePolicy } from "./organization-role.policy"; +import { OrganizationRoleService } from "./organization-role.service"; +import { MembershipSeedService } from "./seed/membership.seed"; +import { OrganizationSeedService } from "./seed/organization.seed"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + AuthenticationModule, + AuthorizationModule, + ], + providers: [ + OrganizationService, + OrganizationRoleService, + OrganizationFactory, + OrganizationPolicy, + OrganizationRolePolicy, + OrganizationMapper, + OrganizationRoleMapper, + OrganizationSeedService, + MembershipSeedService, + ], + exports: [ + OrganizationService, + OrganizationRoleService, + OrganizationMapper, + OrganizationRoleMapper, + ], +}) +export class OrganizationModule {} diff --git a/packages/core/src/modules/organization/organization.policy.ts b/packages/core/src/modules/organization/organization.policy.ts new file mode 100644 index 0000000..b073294 --- /dev/null +++ b/packages/core/src/modules/organization/organization.policy.ts @@ -0,0 +1,11 @@ +import { Policy, PublicResourcePolicy } from "../auth"; +import { Injectable } from "@nestjs/common"; +import { Organization } from "./organization.entity"; + +@Injectable() +@Policy(Organization) +export class OrganizationPolicy extends PublicResourcePolicy { + override create(): boolean { + return true; + } +} diff --git a/packages/core/src/modules/organization/organization.service.ts b/packages/core/src/modules/organization/organization.service.ts new file mode 100644 index 0000000..67b1979 --- /dev/null +++ b/packages/core/src/modules/organization/organization.service.ts @@ -0,0 +1,206 @@ +import { notFound, User } from "../auth"; +import { type EntityService } from "../../shared"; +import { PrismaService } from "../database"; +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; +import { Organization } from "./organization.entity"; +import { OrganizationMapper } from "./organization.mapper"; + +export type Membership = { + id: string; + organizationId: string; + userId: string; + organizationRoleId: string; + createdAt: Date; + updatedAt: Date; + user: { + id: string; + name: string; + createdAt: Date; + credentials?: { + email: string; + } | null; + }; + role: { + id: string; + name: string; + description: string | null; + color: string | null; + createdAt: Date; + updatedAt: Date; + }; +}; + +export type MembershipSortField = "createdAt" | "userName" | "roleName"; +export type MembershipSortOrder = "asc" | "desc"; + +type OrganizationFilters = Record; + +@Injectable() +export class OrganizationService + implements EntityService +{ + constructor( + private readonly prisma: PrismaService, + private readonly organizationMapper: OrganizationMapper, + ) {} + + async findById(id: string): Promise { + const organization = await this.prisma.organization.findUnique({ + where: { id }, + }); + + return this.organizationMapper.toDomain(organization); + } + + async findMembershipsByOrganization( + organizationId: string, + sortBy: MembershipSortField = "createdAt", + sortOrder: MembershipSortOrder = "asc", + searchTerm?: string, + ): Promise { + const whereClause: Prisma.MembershipWhereInput = { organizationId }; + + // Add search functionality for user names + if (searchTerm?.trim()) { + whereClause.user = { + name: { + contains: searchTerm.trim(), + mode: "insensitive", + }, + }; + } + + const memberships = await this.prisma.membership.findMany({ + where: whereClause, + include: { + user: { + select: { + id: true, + name: true, + createdAt: true, + credentials: { + select: { + email: true, + }, + }, + }, + }, + role: { + select: { + id: true, + name: true, + description: true, + color: true, + createdAt: true, + updatedAt: true, + }, + }, + }, + orderBy: + sortBy === "userName" + ? { user: { name: sortOrder } } + : sortBy === "roleName" + ? { role: { name: sortOrder } } + : { createdAt: sortOrder }, + }); + + return memberships as Membership[]; + } + + async countMembershipsByOrganization( + organizationId: string, + searchTerm?: string, + ): Promise { + const whereClause: Prisma.MembershipWhereInput = { organizationId }; + + // Add search functionality for user names + if (searchTerm?.trim()) { + whereClause.user = { + name: { + contains: searchTerm.trim(), + mode: "insensitive", + }, + }; + } + + return this.prisma.membership.count({ + where: whereClause, + }); + } + + async findByIdOrFail(id: string): Promise { + return (await this.findById(id)) ?? notFound("Organization", "id", id); + } + + async findMany(_filters: OrganizationFilters = {}): Promise { + const organizations = await this.prisma.organization.findMany({ + orderBy: { name: "asc" }, + }); + return this.organizationMapper.mapToDomain(organizations); + } + + async count(_filters: OrganizationFilters = {}): Promise { + return this.prisma.organization.count(); + } + + private buildPrismaData({ name, description }: Organization): { + name: string; + description: string | null; + } { + return { + name, + description: description ?? null, + }; + } + + async save(entity: Organization): Promise { + const { id } = entity; + const data = this.buildPrismaData(entity); + const organization = await this.prisma.organization.upsert({ + where: { id }, + create: { id, ...data }, + update: data, + }); + return this.organizationMapper.toDomain(organization); + } + + async destroy(entity: Organization): Promise { + await this.prisma.organization.delete({ + where: { id: entity.id }, + }); + } + + async addUserToOrganization( + { id: organizationId }: Organization, + { id: userId }: User, + roleId?: string, + ): Promise { + // Default to member role if no role specified + const defaultRoleId = roleId || "member_role_id"; + + await this.prisma.membership.create({ + data: { + organizationId, + userId, + organizationRoleId: defaultRoleId, + }, + }); + + return true; + } + + async removeUserFromOrganization( + { id: organizationId }: Organization, + { id: userId }: User, + ): Promise { + await this.prisma.membership.deleteMany({ + where: { + organizationId, + userId, + }, + }); + + return true; + } +} diff --git a/packages/core/src/modules/organization/seed/membership.seed.ts b/packages/core/src/modules/organization/seed/membership.seed.ts new file mode 100644 index 0000000..648f15d --- /dev/null +++ b/packages/core/src/modules/organization/seed/membership.seed.ts @@ -0,0 +1,134 @@ +import { PrismaService } from "../../database"; +import { faker } from "@faker-js/faker"; +import { Injectable, Logger } from "@nestjs/common"; +import { Seeder } from "../../database/seed/seed.service"; +import { Seeder as SeederDecorator } from "../../database/seed/seeder.decorator"; + +@Injectable() +@SeederDecorator({ name: "Memberships", dependsOn: ["Users", "Organizations"] }) +export class MembershipSeedService implements Seeder { + private readonly logger = new Logger(MembershipSeedService.name); + + constructor(readonly _prisma: PrismaService) {} + + private async ensureMemberRole( + prisma: PrismaService, + ): Promise<{ id: string }> { + const existing = await prisma["organizationRole"].findFirst({ + where: { name: "Member" }, + }); + + if (existing) return { id: existing.id }; + + return prisma["organizationRole"].create({ + data: { name: "Member", description: "Default member role" }, + }); + } + + private async createMembershipIfMissing( + prisma: PrismaService, + userId: string, + organizationId: string, + roleId: string, + ): Promise { + const existing = await prisma["membership"].findFirst({ + where: { userId, organizationId }, + }); + + if (!existing) { + await prisma["membership"].create({ + data: { userId, organizationId, organizationRoleId: roleId }, + }); + } + } + + async seed(prisma: PrismaService): Promise { + this.logger.log("Seeding memberships..."); + + const memberRole = await this.ensureMemberRole(prisma); + + const [allUsers, allOrganizations] = await Promise.all([ + prisma["user"].findMany(), + prisma["organization"].findMany(), + ]); + + if (allUsers.length === 0 || allOrganizations.length === 0) { + this.logger.warn( + "No users or organizations found, skipping membership seeding", + ); + return; + } + + await this.ensureTestUserMemberships( + prisma, + allOrganizations, + memberRole.id, + ); + + const allCredentials = await prisma["credentials"].findMany({ + include: { user: true }, + }); + const testUserCredentials = allCredentials.find( + (c: { email: string }) => c.email === "test@test.test", + ); + const testUserIds = testUserCredentials ? [testUserCredentials.userId] : []; + + await Promise.all( + allUsers.map(async (user: { id: string }) => { + if (testUserIds.includes(user.id)) return; + + const numOrganizations = faker.number.int({ min: 1, max: 2 }); + const selectedOrganizations = faker.helpers.arrayElements( + allOrganizations, + { min: 1, max: numOrganizations }, + ); + + await Promise.all( + selectedOrganizations.map((org: { id: string }) => + this.createMembershipIfMissing( + prisma, + user.id, + org.id, + memberRole.id, + ), + ), + ); + }), + ); + + this.logger.log("Assigned users to organizations"); + } + + private async ensureTestUserMemberships( + prisma: PrismaService, + allOrganizations: Array<{ id: string }>, + roleId: string, + ): Promise { + const testCredentials = await prisma["credentials"].findUnique({ + where: { email: "test@test.test" }, + include: { user: true }, + }); + + if (!testCredentials) { + this.logger.warn( + "Test user not found, skipping test user membership assignment", + ); + return; + } + + const testUserId = testCredentials.userId; + const organizationsForTestUser = allOrganizations.slice(0, 5); + + await Promise.all( + organizationsForTestUser.map(async (org) => { + this.logger.log(`Adding test@test.test to organization ${org.id}`); + await this.createMembershipIfMissing( + prisma, + testUserId, + org.id, + roleId, + ); + }), + ); + } +} diff --git a/packages/core/src/modules/organization/seed/organization.seed.ts b/packages/core/src/modules/organization/seed/organization.seed.ts new file mode 100644 index 0000000..b682126 --- /dev/null +++ b/packages/core/src/modules/organization/seed/organization.seed.ts @@ -0,0 +1,57 @@ +import { PrismaService } from "../../database"; +import { faker } from "@faker-js/faker"; +import { Injectable, Logger } from "@nestjs/common"; +import { Seeder } from "../../database/seed/seed.service"; +import { Seeder as SeederDecorator } from "../../database/seed/seeder.decorator"; + +@Injectable() +@SeederDecorator("Organizations") +export class OrganizationSeedService implements Seeder { + private readonly logger = new Logger(OrganizationSeedService.name); + + constructor(private readonly prisma: PrismaService) {} + + async seed(prisma: PrismaService): Promise { + this.logger.log("Seeding organizations..."); + await this.ensureOrganizations(prisma); + } + + async ensureOrganizations( + prisma: PrismaService = this.prisma, + ): Promise> { + const names = [ + "TechCorp Solutions", + "Innovation Labs", + "Digital Dynamics", + "Future Systems", + "Alpha Technologies", + "Beta Solutions", + "Gamma Industries", + "Delta Enterprises", + "Epsilon Corp", + "Zeta Systems", + "Eta Technologies", + "Theta Labs", + "Iota Innovations", + "Kappa Dynamics", + "Lambda Solutions", + ]; + + const created = await Promise.all( + names.map(async (name) => { + const existing = await prisma["organization"].findFirst({ + where: { name }, + }); + if (existing) { + return { id: existing.id }; + } + const org = await prisma["organization"].create({ + data: { name, description: faker.company.catchPhrase() }, + }); + return { id: org.id }; + }), + ); + + return created; + } +} diff --git a/packages/core/src/modules/profile/onboarding/profile.step.ts b/packages/core/src/modules/profile/onboarding/profile.step.ts new file mode 100644 index 0000000..72449b6 --- /dev/null +++ b/packages/core/src/modules/profile/onboarding/profile.step.ts @@ -0,0 +1,31 @@ +import type { User } from "../../auth"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { OnboardingStep } from "../../onboarding/onboarding-step.decorator"; +import { + type OnboardingStepDefinition, + OnboardingStepStatus, +} from "../../onboarding/onboarding-step.interface"; + +@Injectable() +@OnboardingStep() +export class ProfileOnboardingStep implements OnboardingStepDefinition { + readonly name = "personal-profile"; + readonly order = 2; + readonly dependsOn: string[] = []; + + constructor(private readonly prisma: PrismaService) {} + + async computeStatus(user: User): Promise { + const profile = await this.prisma.profile.findFirst({ + where: { userId: user.id }, + select: { headline: true, locationId: true }, + }); + + if (!profile) return OnboardingStepStatus.NOT_STARTED; + + return profile.headline && profile.locationId + ? OnboardingStepStatus.COMPLETE + : OnboardingStepStatus.IN_PROGRESS; + } +} diff --git a/packages/core/src/modules/profile/profile.entity.ts b/packages/core/src/modules/profile/profile.entity.ts new file mode 100644 index 0000000..0d6dd39 --- /dev/null +++ b/packages/core/src/modules/profile/profile.entity.ts @@ -0,0 +1,14 @@ +import { BaseEntity } from "../../shared"; + +export class ProfileEntity extends BaseEntity { + constructor( + id: string, + public readonly userId: string, + public readonly name: string, + public readonly locationId: string | undefined, + createdAt: Date, + updatedAt: Date, + ) { + super(id, createdAt, updatedAt); + } +} diff --git a/packages/core/src/modules/profile/profile.module.ts b/packages/core/src/modules/profile/profile.module.ts new file mode 100644 index 0000000..b94f151 --- /dev/null +++ b/packages/core/src/modules/profile/profile.module.ts @@ -0,0 +1,28 @@ +import { AuthorizationModule } from "../auth/authorization/authorization.module"; +import { BaseModule } from "../../shared"; +import { DatabaseModule } from "../database"; +import { Module, forwardRef } from "@nestjs/common"; +import { CVTemplateModule } from "../cv-template/cv-template.module"; +import { EducationModule } from "../education/education.module"; +import { EmploymentModule } from "../job-experience/employment/employment.module"; +import { ProfileOnboardingStep } from "./onboarding/profile.step"; +import { ProfilePolicy } from "./profile.policy"; +import { ProfileService } from "./profile.service"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + AuthorizationModule, + forwardRef(() => EducationModule), + forwardRef(() => EmploymentModule), + forwardRef(() => CVTemplateModule), + ], + providers: [ + ProfileService, + ProfilePolicy, + ProfileOnboardingStep, + ], + exports: [ProfileService], +}) +export class ProfileModule {} diff --git a/packages/core/src/modules/profile/profile.policy.ts b/packages/core/src/modules/profile/profile.policy.ts new file mode 100644 index 0000000..294281a --- /dev/null +++ b/packages/core/src/modules/profile/profile.policy.ts @@ -0,0 +1,7 @@ +import { Policy, UserOwnedResourcePolicy } from "../auth"; +import { Injectable } from "@nestjs/common"; +import { ProfileEntity } from "./profile.entity"; + +@Injectable() +@Policy(ProfileEntity) +export class ProfilePolicy extends UserOwnedResourcePolicy {} diff --git a/packages/core/src/modules/profile/profile.service.ts b/packages/core/src/modules/profile/profile.service.ts new file mode 100644 index 0000000..c5b6c5e --- /dev/null +++ b/packages/core/src/modules/profile/profile.service.ts @@ -0,0 +1,219 @@ +import type { User } from "../auth"; +import { TokenEncryptionService } from "../auth"; +import { PrismaService } from "../database"; +import { Injectable, NotFoundException } from "@nestjs/common"; + +interface CreateProfileInput { + name: string; + fullName?: string; + headline?: string; + phone?: string; + address?: string; + postalCode?: string; + locationId?: string; + website?: string; + linkedInUrl?: string; + summary?: string; +} + +interface UpdateProfileInput { + name?: string; + fullName?: string; + headline?: string; + phone?: string; + address?: string; + postalCode?: string; + locationId?: string; + website?: string; + linkedInUrl?: string; + summary?: string; +} + +export interface DecryptedProfile { + id: string; + userId: string; + name: string; + fullName: string | null; + headline: string | null; + phone: string | null; + address: string | null; + postalCode: string | null; + locationId: string | null; + website: string | null; + linkedInUrl: string | null; + summary: string | null; + createdAt: Date; + updatedAt: Date; +} + +@Injectable() +export class ProfileService { + constructor( + private readonly prisma: PrismaService, + private readonly encryption: TokenEncryptionService, + ) {} + + async createProfile( + userId: string, + data: CreateProfileInput, + ): Promise { + const encrypted = this.encryptFields(data); + + const profile = await this.prisma.profile.create({ + data: { + userId, + name: data.name, + ...encrypted, + }, + }); + + return this.decrypt(profile); + } + + async getProfilesForUser(userId: string): Promise { + const profiles = await this.prisma.profile.findMany({ + where: { userId }, + orderBy: { createdAt: "asc" }, + }); + + return profiles.map((p) => this.decrypt(p)); + } + + async findByIdOrFail(profileId: string): Promise { + const profile = await this.prisma.profile.findUnique({ + where: { id: profileId }, + }); + + if (!profile) throw new NotFoundException("Profile not found"); + + return this.decrypt(profile); + } + + async findByIdAndUserOrFail( + profileId: string, + userId: string, + ): Promise { + const profile = await this.prisma.profile.findFirst({ + where: { id: profileId, userId }, + }); + + if (!profile) throw new NotFoundException("Profile not found"); + + return this.decrypt(profile); + } + + async updateProfile( + profileId: string, + data: UpdateProfileInput, + ): Promise { + const encrypted = this.encryptFields(data); + + if (data.name !== undefined) { + encrypted["name"] = data.name; + } + + const profile = await this.prisma.profile.update({ + where: { id: profileId }, + data: encrypted, + }); + + return this.decrypt(profile); + } + + async deleteProfile(profileId: string): Promise { + await this.prisma.profile.delete({ + where: { id: profileId }, + }); + return true; + } + + async getOrCreateDefaultProfile(userId: string): Promise { + const existing = await this.prisma.profile.findFirst({ + where: { userId }, + orderBy: { createdAt: "asc" }, + }); + + if (existing) return this.decrypt(existing); + + return this.createProfile(userId, { name: "Default" }); + } + + async hasKeyFields(userId: string): Promise { + const profile = await this.prisma.profile.findFirst({ + where: { userId }, + select: { headline: true, locationId: true }, + }); + + return profile !== null && profile.headline !== null && profile.locationId !== null; + } + + private decrypt(profile: { + id: string; + userId: string; + name: string; + fullName: string | null; + headline: string | null; + encryptedPhone: string | null; + encryptedAddress: string | null; + encryptedPostalCode: string | null; + locationId: string | null; + website: string | null; + linkedInUrl: string | null; + summary: string | null; + createdAt: Date; + updatedAt: Date; + }): DecryptedProfile { + return { + id: profile.id, + userId: profile.userId, + name: profile.name, + fullName: profile.fullName, + headline: profile.headline, + phone: profile.encryptedPhone + ? this.encryption.decrypt(profile.encryptedPhone) + : null, + address: profile.encryptedAddress + ? this.encryption.decrypt(profile.encryptedAddress) + : null, + postalCode: profile.encryptedPostalCode + ? this.encryption.decrypt(profile.encryptedPostalCode) + : null, + locationId: profile.locationId, + website: profile.website, + linkedInUrl: profile.linkedInUrl, + summary: profile.summary, + createdAt: profile.createdAt, + updatedAt: profile.updatedAt, + }; + } + + private encryptFields( + data: Partial, + ): Record { + const result: Record = {}; + + if (data.fullName !== undefined) result["fullName"] = data.fullName || null; + if (data.headline !== undefined) result["headline"] = data.headline || null; + if (data.locationId !== undefined) + result["locationId"] = data.locationId || null; + if (data.website !== undefined) result["website"] = data.website || null; + if (data.linkedInUrl !== undefined) + result["linkedInUrl"] = data.linkedInUrl || null; + if (data.summary !== undefined) result["summary"] = data.summary || null; + + if (data.phone !== undefined) + result["encryptedPhone"] = data.phone + ? this.encryption.encrypt(data.phone) + : null; + if (data.address !== undefined) + result["encryptedAddress"] = data.address + ? this.encryption.encrypt(data.address) + : null; + if (data.postalCode !== undefined) + result["encryptedPostalCode"] = data.postalCode + ? this.encryption.encrypt(data.postalCode) + : null; + + return result; + } +} diff --git a/packages/core/src/modules/user-settings/onboarding/ai-preference.step.ts b/packages/core/src/modules/user-settings/onboarding/ai-preference.step.ts new file mode 100644 index 0000000..0300fba --- /dev/null +++ b/packages/core/src/modules/user-settings/onboarding/ai-preference.step.ts @@ -0,0 +1,28 @@ +import type { User } from "../../auth"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { OnboardingStep } from "../../onboarding/onboarding-step.decorator"; +import { + type OnboardingStepDefinition, + OnboardingStepStatus, +} from "../../onboarding/onboarding-step.interface"; + +@Injectable() +@OnboardingStep() +export class AiPreferenceOnboardingStep implements OnboardingStepDefinition { + readonly name = "ai-preference"; + readonly order = 0; + readonly dependsOn: string[] = []; + + constructor(private readonly prisma: PrismaService) {} + + async computeStatus(user: User): Promise { + const settings = await this.prisma.userAiSettings.findUnique({ + where: { userId: user.id }, + }); + + return settings + ? OnboardingStepStatus.COMPLETE + : OnboardingStepStatus.NOT_STARTED; + } +} diff --git a/packages/core/src/modules/user-settings/user-ai-settings.module.ts b/packages/core/src/modules/user-settings/user-ai-settings.module.ts new file mode 100644 index 0000000..9c8e874 --- /dev/null +++ b/packages/core/src/modules/user-settings/user-ai-settings.module.ts @@ -0,0 +1,20 @@ +import { AIModule, type AIProviderType } from "@cv/ai-provider"; +import { DatabaseModule } from "../database"; +import { Module } from "@nestjs/common"; +import { AiPreferenceOnboardingStep } from "./onboarding/ai-preference.step"; +import { UserAiSettingsService } from "./user-ai-settings.service"; + +@Module({ + imports: [ + DatabaseModule, + AIModule.forRoot({ + type: (process.env["AI_PROVIDER"] as AIProviderType) || "llama-cpp", + }), + ], + providers: [ + UserAiSettingsService, + AiPreferenceOnboardingStep, + ], + exports: [UserAiSettingsService], +}) +export class UserAiSettingsModule {} diff --git a/packages/core/src/modules/user-settings/user-ai-settings.service.ts b/packages/core/src/modules/user-settings/user-ai-settings.service.ts new file mode 100644 index 0000000..8a5646b --- /dev/null +++ b/packages/core/src/modules/user-settings/user-ai-settings.service.ts @@ -0,0 +1,231 @@ +import type { User } from "../auth"; +import { TokenEncryptionService } from "../auth"; +import { PrismaService } from "../database"; +import { + BadRequestException, + Injectable, + NotFoundException, +} from "@nestjs/common"; +import type { AiPreference } from "@prisma/client"; + +@Injectable() +export class UserAiSettingsService { + constructor( + private readonly prisma: PrismaService, + private readonly encryption: TokenEncryptionService, + ) {} + + async getSettings(user: User) { + return this.prisma.userAiSettings.findUnique({ + where: { userId: user.id }, + }); + } + + async getOrCreateSettings(user: User) { + return this.prisma.userAiSettings.upsert({ + where: { userId: user.id }, + create: { userId: user.id, aiPreference: "NO_AI" }, + update: {}, + }); + } + + async updatePreference(user: User, preference: AiPreference) { + const settings = await this.getOrCreateSettings(user); + + const update: { aiPreference: AiPreference; activeProviderId?: null } = { + aiPreference: preference, + }; + + if (preference !== "BYOK" && settings.activeProviderId) { + update.activeProviderId = null; + } + + return this.prisma.userAiSettings.update({ + where: { userId: user.id }, + data: update, + }); + } + + async setActiveProvider(user: User, providerId: string) { + const provider = await this.prisma.userAiProvider.findFirst({ + where: { id: providerId, userId: user.id }, + }); + + if (!provider) { + throw new NotFoundException( + "Provider not found or does not belong to you", + ); + } + + return this.prisma.userAiSettings.upsert({ + where: { userId: user.id }, + create: { + userId: user.id, + aiPreference: "BYOK", + activeProviderId: providerId, + }, + update: { + aiPreference: "BYOK", + activeProviderId: providerId, + }, + }); + } + + async listProviders(user: User) { + return this.prisma.userAiProvider.findMany({ + where: { userId: user.id }, + orderBy: { createdAt: "desc" }, + }); + } + + async addProvider( + user: User, + input: { + label: string; + providerType: string; + apiKey: string; + model?: string; + baseUrl?: string; + setActive?: boolean; + }, + ) { + const keyLastFour = input.apiKey.slice(-4); + const encryptedApiKey = this.encryption.encrypt(input.apiKey); + + const provider = await this.prisma.userAiProvider.create({ + data: { + userId: user.id, + label: input.label, + providerType: input.providerType, + encryptedApiKey, + keyLastFour, + model: input.model ?? null, + baseUrl: input.baseUrl ?? null, + }, + }); + + const settings = await this.getOrCreateSettings(user); + const shouldActivate = input.setActive || !settings.activeProviderId; + + if (shouldActivate) { + await this.prisma.userAiSettings.update({ + where: { userId: user.id }, + data: { activeProviderId: provider.id, aiPreference: "BYOK" }, + }); + } + + return provider; + } + + async removeProvider(user: User, providerId: string) { + const provider = await this.prisma.userAiProvider.findFirst({ + where: { id: providerId, userId: user.id }, + }); + + if (!provider) { + throw new NotFoundException( + "Provider not found or does not belong to you", + ); + } + + await this.prisma.userAiProvider.delete({ where: { id: providerId } }); + + const settings = await this.prisma.userAiSettings.findUnique({ + where: { userId: user.id }, + }); + + const wasActive = settings?.activeProviderId === providerId; + + if (wasActive) { + const remaining = await this.prisma.userAiProvider.findFirst({ + where: { userId: user.id }, + orderBy: { createdAt: "desc" }, + }); + + await this.prisma.userAiSettings.update({ + where: { userId: user.id }, + data: remaining + ? { activeProviderId: remaining.id } + : { activeProviderId: null, aiPreference: "PLATFORM" }, + }); + } + + return { wasActive }; + } + + async replaceProviderKey(user: User, providerId: string, newApiKey: string) { + const provider = await this.prisma.userAiProvider.findFirst({ + where: { id: providerId, userId: user.id }, + }); + + if (!provider) { + throw new NotFoundException( + "Provider not found or does not belong to you", + ); + } + + return this.prisma.userAiProvider.update({ + where: { id: providerId }, + data: { + encryptedApiKey: this.encryption.encrypt(newApiKey), + keyLastFour: newApiKey.slice(-4), + }, + }); + } + + async updateProviderMeta( + user: User, + providerId: string, + input: { label?: string; model?: string; baseUrl?: string }, + ) { + const provider = await this.prisma.userAiProvider.findFirst({ + where: { id: providerId, userId: user.id }, + }); + + if (!provider) { + throw new NotFoundException( + "Provider not found or does not belong to you", + ); + } + + return this.prisma.userAiProvider.update({ + where: { id: providerId }, + data: { + ...(input.label !== undefined && { label: input.label }), + ...(input.model !== undefined && { model: input.model }), + ...(input.baseUrl !== undefined && { baseUrl: input.baseUrl }), + }, + }); + } + + /** + * Resolve the active provider's decrypted config for internal use. + * Not exposed via GraphQL. + */ + async resolveActiveProvider(user: User) { + const settings = await this.getOrCreateSettings(user); + + if (!settings.activeProviderId) { + throw new BadRequestException( + "No active AI provider. Configure one in profile settings.", + ); + } + + const provider = await this.prisma.userAiProvider.findFirst({ + where: { id: settings.activeProviderId, userId: user.id }, + }); + + if (!provider) { + throw new BadRequestException( + "Active AI provider no longer exists. Configure a new one in profile settings.", + ); + } + + return { + providerType: provider.providerType, + decryptedApiKey: this.encryption.decrypt(provider.encryptedApiKey), + model: provider.model, + baseUrl: provider.baseUrl, + }; + } +} diff --git a/packages/core/src/modules/user/seed/user.seed.ts b/packages/core/src/modules/user/seed/user.seed.ts new file mode 100644 index 0000000..dc99d6a --- /dev/null +++ b/packages/core/src/modules/user/seed/user.seed.ts @@ -0,0 +1,131 @@ +import { CredentialsService, UserService } from "../../auth"; +import { PrismaService } from "../../database"; +import { Injectable, Logger } from "@nestjs/common"; +import { Seeder } from "../../database/seed/seed.service"; +import { Seeder as SeederDecorator } from "../../database/seed/seeder.decorator"; + +@Injectable() +@SeederDecorator("Users") +export class UserSeedService implements Seeder { + private readonly logger = new Logger(UserSeedService.name); + + constructor( + private readonly _prisma: PrismaService, + private readonly userService: UserService, + private readonly credentialsService: CredentialsService, + ) {} + + async seed(prisma: PrismaService): Promise { + this.logger.log("Seeding users..."); + await this.ensureTestUser(); + await this.ensureAdditionalTestUsers(); + } + + async ensureTestUser(): Promise<{ id: string; email: string; name: string }> { + const testEmail = "test@test.test"; + let credentials = await this.credentialsService.findByEmail(testEmail); + + if (!credentials) { + this.logger.log("Creating test user..."); + const user = await this.userService.create("Test User"); + credentials = await this.credentialsService.create( + user.id, + testEmail, + // bcrypt hash for "password" + "$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", + ); + await this._prisma["credentials"].update({ + where: { userId: user.id }, + data: { emailVerifiedAt: new Date() }, + }); + } + + const user = await this.userService.findByIdOrFail(credentials.userId); + + await this._prisma["user"].update({ + where: { id: user.id }, + data: { role: "ADMIN" }, + }); + + await this.ensureDefaultProfile(user.id, "Test User"); + + return { + id: user.id, + email: credentials.email, + name: user.name, + }; + } + + async ensureAdditionalTestUsers(): Promise< + Array<{ id: string; email: string; name: string }> + > { + const testUsers = [ + { email: "alice@example.com", name: "Alice Johnson" }, + { email: "bob@example.com", name: "Bob Smith" }, + { email: "carol@example.com", name: "Carol Davis" }, + { email: "david@example.com", name: "David Wilson" }, + { email: "eve@example.com", name: "Eve Brown" }, + { email: "frank@example.com", name: "Frank Miller" }, + { email: "grace@example.com", name: "Grace Taylor" }, + { email: "henry@example.com", name: "Henry Anderson" }, + { email: "ivy@example.com", name: "Ivy Thomas" }, + { email: "jack@example.com", name: "Jack Jackson" }, + { email: "karen@example.com", name: "Karen White" }, + { email: "leo@example.com", name: "Leo Harris" }, + { email: "mary@example.com", name: "Mary Martin" }, + { email: "nick@example.com", name: "Nick Garcia" }, + { email: "olivia@example.com", name: "Olivia Martinez" }, + ]; + + const createdUsers = await Promise.all( + testUsers.map(async (userData) => { + let credentials = await this.credentialsService.findByEmail( + userData.email, + ); + + if (!credentials) { + this.logger.log(`Creating test user: ${userData.name}`); + const { id: userId } = await this.userService.create(userData.name); + credentials = await this.credentialsService.create( + userId, + userData.email, + // bcrypt hash for "password" + "$2b$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi", + ); + await this._prisma["credentials"].update({ + where: { userId }, + data: { emailVerifiedAt: new Date() }, + }); + } + + const user = await this.userService.findByIdOrFail(credentials.userId); + + await this.ensureDefaultProfile(user.id, userData.name); + + return { + id: user.id, + email: credentials.email, + name: user.name, + }; + }), + ); + + return createdUsers; + } + + private async ensureDefaultProfile( + userId: string, + name: string, + ): Promise { + const existing = await this._prisma["profile"].findFirst({ + where: { userId }, + }); + + if (existing) return; + + this.logger.log(`Creating default profile for ${name}`); + await this._prisma["profile"].create({ + data: { userId, name: "Default" }, + }); + } +} diff --git a/packages/core/src/modules/vacancies/contract-type/contract-type.dataloader.ts b/packages/core/src/modules/vacancies/contract-type/contract-type.dataloader.ts new file mode 100644 index 0000000..9e6fb03 --- /dev/null +++ b/packages/core/src/modules/vacancies/contract-type/contract-type.dataloader.ts @@ -0,0 +1,15 @@ +import { BaseDataLoaderService } from "../../../shared"; +import { Injectable, Scope } from "@nestjs/common"; +import { ContractType } from "./contract-type.entity"; +import { ContractTypeService } from "./contract-type.service"; + +@Injectable({ scope: Scope.REQUEST }) +export class ContractTypeDataLoaderService extends BaseDataLoaderService { + constructor(service: ContractTypeService) { + super(async (ids: readonly string[]) => { + const items = await service.findMany({ id: [...ids] }); + const map = new Map(items.map((item) => [item.id, item])); + return ids.map((id) => map.get(id) ?? null); + }); + } +} diff --git a/packages/core/src/modules/vacancies/contract-type/contract-type.entity.ts b/packages/core/src/modules/vacancies/contract-type/contract-type.entity.ts new file mode 100644 index 0000000..a540a06 --- /dev/null +++ b/packages/core/src/modules/vacancies/contract-type/contract-type.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "../../../shared"; + +export class ContractType extends NamedEntity {} diff --git a/packages/core/src/modules/vacancies/contract-type/contract-type.mapper.ts b/packages/core/src/modules/vacancies/contract-type/contract-type.mapper.ts new file mode 100644 index 0000000..c182a67 --- /dev/null +++ b/packages/core/src/modules/vacancies/contract-type/contract-type.mapper.ts @@ -0,0 +1,20 @@ +import { createNamedEntityMapper } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { ContractType as PrismaContractType } from "@prisma/client"; +import { ContractType } from "./contract-type.entity"; + +@Injectable() +export class ContractTypeMapper { + private mapper = createNamedEntityMapper(ContractType); + + toDomain(prisma: null): null; + toDomain(prisma: PrismaContractType): ContractType; + toDomain(prisma: PrismaContractType | null): ContractType | null; + toDomain(prisma: PrismaContractType | null): ContractType | null { + return this.mapper.toDomain(prisma); + } + + mapToDomain(prisma: PrismaContractType[]): ContractType[] { + return this.mapper.mapToDomain(prisma); + } +} diff --git a/packages/core/src/modules/vacancies/contract-type/contract-type.module.ts b/packages/core/src/modules/vacancies/contract-type/contract-type.module.ts new file mode 100644 index 0000000..3ffd8cd --- /dev/null +++ b/packages/core/src/modules/vacancies/contract-type/contract-type.module.ts @@ -0,0 +1,12 @@ +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { ContractTypeDataLoaderService } from "./contract-type.dataloader"; +import { ContractTypeMapper } from "./contract-type.mapper"; +import { ContractTypeService } from "./contract-type.service"; + +@Module({ + imports: [DatabaseModule], + providers: [ContractTypeService, ContractTypeMapper, ContractTypeDataLoaderService], + exports: [ContractTypeService, ContractTypeDataLoaderService], +}) +export class ContractTypeModule {} diff --git a/packages/core/src/modules/vacancies/contract-type/contract-type.service.ts b/packages/core/src/modules/vacancies/contract-type/contract-type.service.ts new file mode 100644 index 0000000..795e6a9 --- /dev/null +++ b/packages/core/src/modules/vacancies/contract-type/contract-type.service.ts @@ -0,0 +1,12 @@ +import { NamedEntityService } from "../../../shared"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { ContractType } from "./contract-type.entity"; +import { ContractTypeMapper } from "./contract-type.mapper"; + +@Injectable() +export class ContractTypeService extends NamedEntityService { + constructor(prisma: PrismaService, mapper: ContractTypeMapper) { + super(prisma, mapper, prisma.contractType, "ContractType"); + } +} diff --git a/packages/core/src/modules/vacancies/job-type/job-type.dto.ts b/packages/core/src/modules/vacancies/job-type/job-type.dto.ts new file mode 100644 index 0000000..13a7080 --- /dev/null +++ b/packages/core/src/modules/vacancies/job-type/job-type.dto.ts @@ -0,0 +1,9 @@ +export interface CreateJobTypeDto { + name: string; + description?: string; +} + +export interface UpdateJobTypeDto { + name?: string; + description?: string; +} diff --git a/packages/core/src/modules/vacancies/job-type/job-type.entity.ts b/packages/core/src/modules/vacancies/job-type/job-type.entity.ts new file mode 100644 index 0000000..fd9a8bd --- /dev/null +++ b/packages/core/src/modules/vacancies/job-type/job-type.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "../../../shared"; + +export class JobType extends NamedEntity {} diff --git a/packages/core/src/modules/vacancies/job-type/job-type.factory.ts b/packages/core/src/modules/vacancies/job-type/job-type.factory.ts new file mode 100644 index 0000000..a852b66 --- /dev/null +++ b/packages/core/src/modules/vacancies/job-type/job-type.factory.ts @@ -0,0 +1,23 @@ +import { ClockService, Factory, UuidFactoryService } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { CreateJobTypeDto } from "./job-type.dto"; +import { JobType } from "./job-type.entity"; + +@Injectable() +export class JobTypeFactory implements Factory { + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(dto: CreateJobTypeDto): JobType { + const now = this.clock.now(); + return new JobType( + this.uuidFactory.generate(), + dto.name, + now, + now, + dto.description, + ); + } +} diff --git a/packages/core/src/modules/vacancies/job-type/job-type.mapper.ts b/packages/core/src/modules/vacancies/job-type/job-type.mapper.ts new file mode 100644 index 0000000..0b618f6 --- /dev/null +++ b/packages/core/src/modules/vacancies/job-type/job-type.mapper.ts @@ -0,0 +1,20 @@ +import { createNamedEntityMapper } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { JobType as PrismaJobType } from "@prisma/client"; +import { JobType } from "./job-type.entity"; + +@Injectable() +export class JobTypeMapper { + private mapper = createNamedEntityMapper(JobType); + + toDomain(prismaEntity: null): null; + toDomain(prismaEntity: PrismaJobType): JobType; + toDomain(prismaEntity: PrismaJobType | null): JobType | null; + toDomain(prismaEntity: PrismaJobType | null): JobType | null { + return this.mapper.toDomain(prismaEntity); + } + + mapToDomain(prismaEntities: PrismaJobType[]): JobType[] { + return this.mapper.mapToDomain(prismaEntities); + } +} diff --git a/packages/core/src/modules/vacancies/job-type/job-type.module.ts b/packages/core/src/modules/vacancies/job-type/job-type.module.ts new file mode 100644 index 0000000..d7e66f7 --- /dev/null +++ b/packages/core/src/modules/vacancies/job-type/job-type.module.ts @@ -0,0 +1,13 @@ +import { BaseModule } from "../../../shared"; +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { JobTypeFactory } from "./job-type.factory"; +import { JobTypeMapper } from "./job-type.mapper"; +import { JobTypeService } from "./job-type.service"; + +@Module({ + imports: [DatabaseModule, BaseModule], + providers: [JobTypeService, JobTypeMapper, JobTypeFactory], + exports: [JobTypeService, JobTypeMapper, JobTypeFactory], +}) +export class JobTypeModule {} diff --git a/packages/core/src/modules/vacancies/job-type/job-type.service.ts b/packages/core/src/modules/vacancies/job-type/job-type.service.ts new file mode 100644 index 0000000..4c9ec55 --- /dev/null +++ b/packages/core/src/modules/vacancies/job-type/job-type.service.ts @@ -0,0 +1,12 @@ +import { NamedEntityService } from "../../../shared"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { JobType } from "./job-type.entity"; +import { JobTypeMapper } from "./job-type.mapper"; + +@Injectable() +export class JobTypeService extends NamedEntityService { + constructor(prisma: PrismaService, jobTypeMapper: JobTypeMapper) { + super(prisma, jobTypeMapper, prisma.jobType, "JobType"); + } +} diff --git a/packages/core/src/modules/vacancies/location/location.dataloader.ts b/packages/core/src/modules/vacancies/location/location.dataloader.ts new file mode 100644 index 0000000..d0ace2b --- /dev/null +++ b/packages/core/src/modules/vacancies/location/location.dataloader.ts @@ -0,0 +1,15 @@ +import { BaseDataLoaderService } from "../../../shared"; +import { Injectable, Scope } from "@nestjs/common"; +import { Location } from "./location.entity"; +import { LocationService } from "./location.service"; + +@Injectable({ scope: Scope.REQUEST }) +export class LocationDataLoaderService extends BaseDataLoaderService { + constructor(service: LocationService) { + super(async (ids: readonly string[]) => { + const items = await service.findMany({ id: [...ids] }); + const map = new Map(items.map((item) => [item.id, item])); + return ids.map((id) => map.get(id) ?? null); + }); + } +} diff --git a/packages/core/src/modules/vacancies/location/location.entity.ts b/packages/core/src/modules/vacancies/location/location.entity.ts new file mode 100644 index 0000000..b855c68 --- /dev/null +++ b/packages/core/src/modules/vacancies/location/location.entity.ts @@ -0,0 +1,33 @@ +import { BaseEntity } from "../../../shared"; + +export const LocationType = { + COUNTRY: "COUNTRY", + PROVINCE: "PROVINCE", + CITY: "CITY", + DISTRICT: "DISTRICT", +} as const; + +export type LocationType = (typeof LocationType)[keyof typeof LocationType]; + +export class Location extends BaseEntity { + name: string; + type: LocationType; + code?: string | undefined; + parentId?: string | undefined; + + constructor( + id: string, + name: string, + type: LocationType, + createdAt: Date, + updatedAt: Date, + code?: string, + parentId?: string, + ) { + super(id, createdAt, updatedAt); + this.name = name; + this.type = type; + this.code = code; + this.parentId = parentId; + } +} diff --git a/packages/core/src/modules/vacancies/location/location.mapper.ts b/packages/core/src/modules/vacancies/location/location.mapper.ts new file mode 100644 index 0000000..2c93931 --- /dev/null +++ b/packages/core/src/modules/vacancies/location/location.mapper.ts @@ -0,0 +1,26 @@ +import { Injectable } from "@nestjs/common"; +import type { Location as PrismaLocation } from "@prisma/client"; +import { Location } from "./location.entity"; + +@Injectable() +export class LocationMapper { + toDomain(prisma: null): null; + toDomain(prisma: PrismaLocation): Location; + toDomain(prisma: PrismaLocation | null): Location | null; + toDomain(prisma: PrismaLocation | null): Location | null { + if (!prisma) return null; + return new Location( + prisma.id, + prisma.name, + prisma.type, + prisma.createdAt, + prisma.updatedAt, + prisma.code ?? undefined, + prisma.parentId ?? undefined, + ); + } + + mapToDomain(items: PrismaLocation[]): Location[] { + return items.map((item) => this.toDomain(item)).filter((item): item is Location => item !== null); + } +} diff --git a/packages/core/src/modules/vacancies/location/location.module.ts b/packages/core/src/modules/vacancies/location/location.module.ts new file mode 100644 index 0000000..b9bd05c --- /dev/null +++ b/packages/core/src/modules/vacancies/location/location.module.ts @@ -0,0 +1,12 @@ +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { LocationDataLoaderService } from "./location.dataloader"; +import { LocationMapper } from "./location.mapper"; +import { LocationService } from "./location.service"; + +@Module({ + imports: [DatabaseModule], + providers: [LocationService, LocationMapper, LocationDataLoaderService], + exports: [LocationService, LocationDataLoaderService], +}) +export class LocationModule {} diff --git a/packages/core/src/modules/vacancies/location/location.service.ts b/packages/core/src/modules/vacancies/location/location.service.ts new file mode 100644 index 0000000..6750f77 --- /dev/null +++ b/packages/core/src/modules/vacancies/location/location.service.ts @@ -0,0 +1,23 @@ +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { Location } from "./location.entity"; +import { LocationMapper } from "./location.mapper"; + +@Injectable() +export class LocationService { + constructor( + private readonly prisma: PrismaService, + private readonly mapper: LocationMapper, + ) {} + + async findById(id: string): Promise { + const location = await this.prisma.location.findUnique({ where: { id } }); + return this.mapper.toDomain(location); + } + + async findMany(filter?: { id?: string[] }): Promise { + const where = filter?.id ? { id: { in: filter.id } } : {}; + const items = await this.prisma.location.findMany({ where }); + return this.mapper.mapToDomain(items); + } +} diff --git a/packages/core/src/modules/vacancies/rate-type/rate-type.dataloader.ts b/packages/core/src/modules/vacancies/rate-type/rate-type.dataloader.ts new file mode 100644 index 0000000..2be034c --- /dev/null +++ b/packages/core/src/modules/vacancies/rate-type/rate-type.dataloader.ts @@ -0,0 +1,15 @@ +import { BaseDataLoaderService } from "../../../shared"; +import { Injectable, Scope } from "@nestjs/common"; +import { RateType } from "./rate-type.entity"; +import { RateTypeService } from "./rate-type.service"; + +@Injectable({ scope: Scope.REQUEST }) +export class RateTypeDataLoaderService extends BaseDataLoaderService { + constructor(service: RateTypeService) { + super(async (ids: readonly string[]) => { + const items = await service.findMany({ id: [...ids] }); + const map = new Map(items.map((item) => [item.id, item])); + return ids.map((id) => map.get(id) ?? null); + }); + } +} diff --git a/packages/core/src/modules/vacancies/rate-type/rate-type.entity.ts b/packages/core/src/modules/vacancies/rate-type/rate-type.entity.ts new file mode 100644 index 0000000..5b7f4d6 --- /dev/null +++ b/packages/core/src/modules/vacancies/rate-type/rate-type.entity.ts @@ -0,0 +1,3 @@ +import { NamedEntity } from "../../../shared"; + +export class RateType extends NamedEntity {} diff --git a/packages/core/src/modules/vacancies/rate-type/rate-type.mapper.ts b/packages/core/src/modules/vacancies/rate-type/rate-type.mapper.ts new file mode 100644 index 0000000..fc03eb0 --- /dev/null +++ b/packages/core/src/modules/vacancies/rate-type/rate-type.mapper.ts @@ -0,0 +1,20 @@ +import { createNamedEntityMapper } from "../../../shared"; +import { Injectable } from "@nestjs/common"; +import type { RateType as PrismaRateType } from "@prisma/client"; +import { RateType } from "./rate-type.entity"; + +@Injectable() +export class RateTypeMapper { + private mapper = createNamedEntityMapper(RateType); + + toDomain(prisma: null): null; + toDomain(prisma: PrismaRateType): RateType; + toDomain(prisma: PrismaRateType | null): RateType | null; + toDomain(prisma: PrismaRateType | null): RateType | null { + return this.mapper.toDomain(prisma); + } + + mapToDomain(prisma: PrismaRateType[]): RateType[] { + return this.mapper.mapToDomain(prisma); + } +} diff --git a/packages/core/src/modules/vacancies/rate-type/rate-type.module.ts b/packages/core/src/modules/vacancies/rate-type/rate-type.module.ts new file mode 100644 index 0000000..8f07ab3 --- /dev/null +++ b/packages/core/src/modules/vacancies/rate-type/rate-type.module.ts @@ -0,0 +1,12 @@ +import { DatabaseModule } from "../../database"; +import { Module } from "@nestjs/common"; +import { RateTypeDataLoaderService } from "./rate-type.dataloader"; +import { RateTypeMapper } from "./rate-type.mapper"; +import { RateTypeService } from "./rate-type.service"; + +@Module({ + imports: [DatabaseModule], + providers: [RateTypeService, RateTypeMapper, RateTypeDataLoaderService], + exports: [RateTypeService, RateTypeDataLoaderService], +}) +export class RateTypeModule {} diff --git a/packages/core/src/modules/vacancies/rate-type/rate-type.service.ts b/packages/core/src/modules/vacancies/rate-type/rate-type.service.ts new file mode 100644 index 0000000..6f84dc8 --- /dev/null +++ b/packages/core/src/modules/vacancies/rate-type/rate-type.service.ts @@ -0,0 +1,12 @@ +import { NamedEntityService } from "../../../shared"; +import { PrismaService } from "../../database"; +import { Injectable } from "@nestjs/common"; +import { RateType } from "./rate-type.entity"; +import { RateTypeMapper } from "./rate-type.mapper"; + +@Injectable() +export class RateTypeService extends NamedEntityService { + constructor(prisma: PrismaService, mapper: RateTypeMapper) { + super(prisma, mapper, prisma.rateType, "RateType"); + } +} diff --git a/packages/core/src/modules/vacancies/seed/vacancy.seed.ts b/packages/core/src/modules/vacancies/seed/vacancy.seed.ts new file mode 100644 index 0000000..b85a78c --- /dev/null +++ b/packages/core/src/modules/vacancies/seed/vacancy.seed.ts @@ -0,0 +1,92 @@ +import { PrismaService } from "../../database"; +import { faker } from "@faker-js/faker"; +import { Injectable, Logger } from "@nestjs/common"; +import { Seeder } from "../../database/seed/seed.service"; +import { Seeder as SeederDecorator } from "../../database/seed/seeder.decorator"; + +@Injectable() +@SeederDecorator({ name: "Public Vacancies", dependsOn: ["Users", "Reference Data"] }) +export class VacancySeedService implements Seeder { + private readonly logger = new Logger(VacancySeedService.name); + + constructor(readonly _prisma: PrismaService) {} + + async seed(prisma: PrismaService): Promise { + this.logger.log("Seeding public vacancies..."); + + // Get reference data + const [companies, roles, levels, jobTypes, users] = await Promise.all([ + prisma["company"].findMany(), + prisma["role"].findMany(), + prisma["level"].findMany(), + prisma["jobType"].findMany(), + prisma["user"].findMany({ take: 10 }), + ]); + + if ( + companies.length === 0 || + roles.length === 0 || + levels.length === 0 || + jobTypes.length === 0 || + users.length === 0 + ) { + this.logger.warn( + "Missing reference data. Skipping vacancy seeding. Make sure reference data is seeded first.", + ); + return; + } + + // Generate 50 public vacancies + const vacanciesToCreate = Array.from({ length: 50 }, () => { + const company = faker.helpers.arrayElement(companies) as { + id: string; + name: string; + }; + const role = faker.helpers.arrayElement(roles) as { + id: string; + name: string; + }; + const level = faker.helpers.arrayElement(levels) as { id: string }; + const jobType = faker.helpers.arrayElement(jobTypes) as { id: string }; + const owner = faker.helpers.arrayElement(users) as { id: string }; + + const minSalary = faker.datatype.boolean() + ? faker.number.int({ min: 50000, max: 150000 }) + : null; + const maxSalary = minSalary + ? faker.number.int({ min: minSalary, max: minSalary + 50000 }) + : null; + + return { + ownerId: owner.id, + title: `${role.name} - ${company.name}`, + companyId: company.id, + roleId: role.id, + levelId: level.id, + jobTypeId: jobType.id, + description: faker.lorem.paragraphs(3), + requirements: faker.lorem.paragraphs(2), + minSalary, + maxSalary, + applicationUrl: faker.internet.url(), + deadline: faker.date.future(), + isActive: true, + isPublic: true, + }; + }); + + // Create vacancies in batches + const batchSize = 10; + for (let i = 0; i < vacanciesToCreate.length; i += batchSize) { + const batch = vacanciesToCreate.slice(i, i + batchSize); + await Promise.all( + batch.map((data) => prisma["vacancy"].create({ data })), + ); + this.logger.log( + `Created ${Math.min(i + batchSize, vacanciesToCreate.length)}/${vacanciesToCreate.length} vacancies...`, + ); + } + + this.logger.log(`✅ Created ${vacanciesToCreate.length} public vacancies`); + } +} diff --git a/packages/core/src/modules/vacancies/vacancy-filter.types.ts b/packages/core/src/modules/vacancies/vacancy-filter.types.ts new file mode 100644 index 0000000..63aa841 --- /dev/null +++ b/packages/core/src/modules/vacancies/vacancy-filter.types.ts @@ -0,0 +1,10 @@ +export interface VacancyFilter { + searchTerm?: string | null; + jobTypes?: string[] | null; + levels?: string[] | null; + companies?: string[] | null; + locations?: string[] | null; + minSalary?: number | null; + maxSalary?: number | null; + isPublic?: boolean | null; +} diff --git a/packages/core/src/modules/vacancies/vacancy.entity.ts b/packages/core/src/modules/vacancies/vacancy.entity.ts new file mode 100644 index 0000000..456e9ea --- /dev/null +++ b/packages/core/src/modules/vacancies/vacancy.entity.ts @@ -0,0 +1,86 @@ +import { BaseEntity } from "../../shared"; +import { Company } from "../job-experience/company/company.entity"; +import { Level } from "../job-experience/level/level.entity"; +import { Role } from "../job-experience/role/role.entity"; + +export class Vacancy extends BaseEntity { + title: string; + ownerId: string; + company: Company; + role: Role; + level: Level | null; + jobTypeId?: string; + description?: string; + requirements?: string | undefined; + minSalary?: number | undefined; + maxSalary?: number | undefined; + applicationUrl?: string | undefined; + deadline?: Date | undefined; + hoursFrom?: number | undefined; + hoursTo?: number | undefined; + isActive: boolean; + isPublic: boolean; + + // Entity FKs + rateTypeId?: string | undefined; + contractTypeId?: string | undefined; + locationId?: string | undefined; + + // External import fields + externalId?: string | undefined; + source?: string | undefined; + externalApplyUrl?: string | undefined; + publishedAt?: Date | undefined; + + constructor( + id: string, + title: string, + ownerId: string, + company: Company, + role: Role, + createdAt: Date, + updatedAt: Date, + level: Level | null = null, + jobTypeId?: string, + description?: string, + requirements?: string, + _location?: string, + minSalary?: number, + maxSalary?: number, + applicationUrl?: string, + deadline?: Date, + isActive: boolean = true, + isPublic: boolean = false, + ) { + super(id, createdAt, updatedAt); + this.title = title; + this.ownerId = ownerId; + this.company = company; + this.role = role; + this.level = level; + this.isActive = isActive; + this.isPublic = isPublic; + + if (jobTypeId !== undefined) { + this.jobTypeId = jobTypeId; + } + if (description !== undefined) { + this.description = description; + } + if (requirements !== undefined) { + this.requirements = requirements; + } + if (minSalary !== undefined) { + this.minSalary = minSalary; + } + if (maxSalary !== undefined) { + this.maxSalary = maxSalary; + } + if (applicationUrl !== undefined) { + this.applicationUrl = applicationUrl; + } + if (deadline !== undefined) { + this.deadline = deadline; + } + } +} diff --git a/packages/core/src/modules/vacancies/vacancy.factory.ts b/packages/core/src/modules/vacancies/vacancy.factory.ts new file mode 100644 index 0000000..a91569f --- /dev/null +++ b/packages/core/src/modules/vacancies/vacancy.factory.ts @@ -0,0 +1,56 @@ +import { ClockService, Factory, UuidFactoryService } from "../../shared"; +import { Injectable } from "@nestjs/common"; +import { Company } from "../job-experience/company/company.entity"; +import { Level } from "../job-experience/level/level.entity"; +import { Role } from "../job-experience/role/role.entity"; +import { Vacancy } from "./vacancy.entity"; + +export interface CreateVacancyDto { + ownerId: string; + title: string; + company: Company; + role: Role; + level?: Level | null; + jobTypeId?: string; + description?: string; + requirements?: string; + locationId?: string; + minSalary?: number; + maxSalary?: number; + applicationUrl?: string; + deadline?: Date; + isActive?: boolean; + isPublic?: boolean; +} + +@Injectable() +export class VacancyFactory implements Factory { + constructor( + private readonly uuidFactory: UuidFactoryService, + private readonly clock: ClockService, + ) {} + + create(createVacancyDto: CreateVacancyDto): Vacancy { + const now = this.clock.now(); + return new Vacancy( + this.uuidFactory.generate(), + createVacancyDto.title, + createVacancyDto.ownerId, + createVacancyDto.company, + createVacancyDto.role, + now, + now, + createVacancyDto.level ?? null, + createVacancyDto.jobTypeId, + createVacancyDto.description, + createVacancyDto.requirements, + undefined, + createVacancyDto.minSalary, + createVacancyDto.maxSalary, + createVacancyDto.applicationUrl, + createVacancyDto.deadline, + createVacancyDto.isActive ?? true, + createVacancyDto.isPublic ?? false, + ); + } +} diff --git a/packages/core/src/modules/vacancies/vacancy.mapper.ts b/packages/core/src/modules/vacancies/vacancy.mapper.ts new file mode 100644 index 0000000..3ed1d50 --- /dev/null +++ b/packages/core/src/modules/vacancies/vacancy.mapper.ts @@ -0,0 +1,84 @@ +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; +import { CompanyMapper } from "../job-experience/company/company.mapper"; +import { LevelMapper } from "../job-experience/level/level.mapper"; +import { RoleMapper } from "../job-experience/role/role.mapper"; +import { Vacancy } from "./vacancy.entity"; + +type PrismaVacancyWithRelations = Prisma.VacancyGetPayload<{ + include: { + company: true; + role: true; + level: true; + jobType: true; + skills: true; + }; +}>; + +@Injectable() +export class VacancyMapper { + constructor( + private readonly companyMapper: CompanyMapper, + private readonly roleMapper: RoleMapper, + private readonly levelMapper: LevelMapper, + ) {} + + toDomain(prismaVacancy: PrismaVacancyWithRelations | null): Vacancy | null { + if (!prismaVacancy) { + return null; + } + + const company = this.companyMapper.toDomain(prismaVacancy.company); + if (!company) { + throw new Error("Company is required for Vacancy"); + } + + const role = this.roleMapper.toDomain(prismaVacancy.role); + if (!role) { + throw new Error("Role is required for Vacancy"); + } + + const level = prismaVacancy.level + ? this.levelMapper.toDomain(prismaVacancy.level) + : null; + + const vacancy = new Vacancy( + prismaVacancy.id, + prismaVacancy.title, + prismaVacancy.ownerId, + company, + role, + prismaVacancy.createdAt, + prismaVacancy.updatedAt, + level, + prismaVacancy.jobTypeId ?? undefined, + prismaVacancy.description ?? undefined, + prismaVacancy.requirements ?? undefined, + undefined, + prismaVacancy.minSalary ?? undefined, + prismaVacancy.maxSalary ?? undefined, + prismaVacancy.applicationUrl ?? undefined, + prismaVacancy.deadline ?? undefined, + prismaVacancy.isActive, + prismaVacancy.isPublic, + ); + + vacancy.hoursFrom = prismaVacancy.hoursFrom ?? undefined; + vacancy.hoursTo = prismaVacancy.hoursTo ?? undefined; + vacancy.rateTypeId = prismaVacancy.rateTypeId ?? undefined; + vacancy.contractTypeId = prismaVacancy.contractTypeId ?? undefined; + vacancy.locationId = prismaVacancy.locationId ?? undefined; + vacancy.externalId = prismaVacancy.externalId ?? undefined; + vacancy.source = prismaVacancy.source ?? undefined; + vacancy.externalApplyUrl = prismaVacancy.externalApplyUrl ?? undefined; + vacancy.publishedAt = prismaVacancy.publishedAt ?? undefined; + + return vacancy; + } + + mapToDomain(prismaVacancies: PrismaVacancyWithRelations[]): Vacancy[] { + return prismaVacancies + .map((v) => this.toDomain(v)) + .filter((v): v is Vacancy => v !== null); + } +} diff --git a/packages/core/src/modules/vacancies/vacancy.module.ts b/packages/core/src/modules/vacancies/vacancy.module.ts new file mode 100644 index 0000000..4252e69 --- /dev/null +++ b/packages/core/src/modules/vacancies/vacancy.module.ts @@ -0,0 +1,42 @@ +import { AuthorizationModule } from "../auth"; +import { BaseModule } from "../../shared"; +import { DatabaseModule } from "../database"; +import { Module } from "@nestjs/common"; +import { AuthenticationModule } from "../authentication/authentication.module"; +import { CompanyModule } from "../job-experience/company/company.module"; +import { LevelModule } from "../job-experience/level/level.module"; +import { RoleModule } from "../job-experience/role/role.module"; +import { SkillModule } from "../job-experience/skill/skill.module"; +import { ContractTypeModule } from "./contract-type/contract-type.module"; +import { LocationModule } from "./location/location.module"; +import { RateTypeModule } from "./rate-type/rate-type.module"; +import { VacancySeedService } from "./seed/vacancy.seed"; +import { VacancyFactory } from "./vacancy.factory"; +import { VacancyMapper } from "./vacancy.mapper"; +import { VacancyPolicy } from "./vacancy.policy"; +import { VacancyService } from "./vacancy.service"; + +@Module({ + imports: [ + DatabaseModule, + BaseModule, + CompanyModule, + RoleModule, + LevelModule, + SkillModule, + RateTypeModule, + ContractTypeModule, + LocationModule, + AuthenticationModule, + AuthorizationModule, + ], + providers: [ + VacancyService, + VacancyFactory, + VacancyMapper, + VacancyPolicy, + VacancySeedService, + ], + exports: [VacancyService, VacancyMapper], +}) +export class VacancyModule {} diff --git a/packages/core/src/modules/vacancies/vacancy.policy.ts b/packages/core/src/modules/vacancies/vacancy.policy.ts new file mode 100644 index 0000000..9331b69 --- /dev/null +++ b/packages/core/src/modules/vacancies/vacancy.policy.ts @@ -0,0 +1,12 @@ +import type { User } from "../auth"; +import { OwnerOwnedResourcePolicy, Policy } from "../auth"; +import { Injectable } from "@nestjs/common"; +import { Vacancy } from "./vacancy.entity"; + +@Injectable() +@Policy(Vacancy) +export class VacancyPolicy extends OwnerOwnedResourcePolicy { + override view(user: User, resource: Vacancy): boolean | Promise { + return resource.isPublic || resource.ownerId === user.id; + } +} diff --git a/packages/core/src/modules/vacancies/vacancy.service.ts b/packages/core/src/modules/vacancies/vacancy.service.ts new file mode 100644 index 0000000..e08fccf --- /dev/null +++ b/packages/core/src/modules/vacancies/vacancy.service.ts @@ -0,0 +1,196 @@ +import { notFound } from "../auth"; +import { type EntityService, raise } from "../../shared"; +import { PrismaService } from "../database"; +import { ensureArray } from "@cv/utils"; +import { Injectable } from "@nestjs/common"; +import type { Prisma } from "@prisma/client"; +import { VacancyFilter } from "./vacancy-filter.types"; +import { Vacancy } from "./vacancy.entity"; +import { VacancyMapper } from "./vacancy.mapper"; + +type VacancyFilters = { + filter?: VacancyFilter; + userId?: string; + id?: string | string[]; +}; + +@Injectable() +export class VacancyService implements EntityService { + private readonly vacancyInclude = { + company: true, + role: true, + level: true, + jobType: true, + skills: true, + } satisfies Prisma.VacancyInclude; + + constructor( + private readonly prisma: PrismaService, + private readonly vacancyMapper: VacancyMapper, + ) {} + + async findById(id: string): Promise { + const vacancy = await this.prisma.vacancy.findUnique({ + where: { id }, + include: this.vacancyInclude, + }); + + return this.vacancyMapper.toDomain(vacancy); + } + + private buildWhere(filters?: VacancyFilters): Prisma.VacancyWhereInput { + const { filter, userId, id } = filters ?? {}; + const where: Prisma.VacancyWhereInput = {}; + + if (userId) { + where.ownerId = userId; + } + + if (id !== undefined) { + where.id = Array.isArray(id) ? { in: id } : id; + } + + if (!filter) { + return where; + } + + const andConditions: Prisma.VacancyWhereInput[] = []; + + // Search term filter + if (filter.searchTerm) { + const searchFilter = { + contains: filter.searchTerm, + mode: "insensitive", + } as const; + const searchConditions: Prisma.VacancyWhereInput[] = [ + { title: searchFilter }, + { description: searchFilter }, + { + requirements: searchFilter, + }, + { location: { name: searchFilter } }, + ]; + + if (where.OR) { + const existingORConditions = ensureArray(where.OR); + andConditions.push({ OR: existingORConditions }); + delete where.OR; + } + andConditions.push({ OR: searchConditions }); + } + + // Company filter + if (filter.companies && filter.companies.length > 0) { + where.companyId = { in: filter.companies }; + } + + // Location filter + if (filter.locations && filter.locations.length > 0) { + where.locationId = { in: filter.locations }; + } + + // Level filter + if (filter.levels && filter.levels.length > 0) { + where.levelId = { in: filter.levels }; + } + + // Salary range filter - find vacancies where the salary range overlaps with the filter + if (filter.minSalary !== undefined || filter.maxSalary !== undefined) { + // Vacancy overlaps if: (vacancy.minSalary <= filter.maxSalary OR vacancy.minSalary is null) + // AND (vacancy.maxSalary >= filter.minSalary OR vacancy.maxSalary is null) + if (filter.minSalary !== undefined && filter.minSalary !== null) { + andConditions.push({ + OR: [{ maxSalary: { gte: filter.minSalary } }, { maxSalary: null }], + }); + } + + if (filter.maxSalary !== undefined && filter.maxSalary !== null) { + andConditions.push({ + OR: [{ minSalary: { lte: filter.maxSalary } }, { minSalary: null }], + }); + } + } + + // Public filter + if (filter.isPublic !== undefined && filter.isPublic !== null) { + where.isPublic = filter.isPublic; + } + + // Combine all AND conditions + if (andConditions.length > 0) { + const existingAND = where.AND + ? Array.isArray(where.AND) + ? where.AND + : [where.AND] + : []; + where.AND = [...existingAND, ...andConditions]; + } + + return where; + } + + async findMany(filters?: VacancyFilters): Promise { + const where = this.buildWhere(filters); + const items = await this.prisma.vacancy.findMany({ + where, + include: this.vacancyInclude, + orderBy: { createdAt: "desc" }, + }); + + return this.vacancyMapper.mapToDomain(items); + } + + async count(filters?: VacancyFilters): Promise { + return this.prisma.vacancy.count({ where: this.buildWhere(filters) }); + } + + async findByIdOrFail(id: string): Promise { + const vacancy = await this.findById(id); + return vacancy ?? notFound("Vacancy", "id", id); + } + + private buildPrismaData(entity: Vacancy) { + return { + title: entity.title, + companyId: entity.company.id, + roleId: entity.role.id, + levelId: entity.level?.id ?? null, + jobTypeId: entity.jobTypeId ?? null, + description: entity.description ?? null, + requirements: entity.requirements ?? null, + locationId: entity.locationId ?? null, + minSalary: entity.minSalary ?? null, + maxSalary: entity.maxSalary ?? null, + applicationUrl: entity.applicationUrl ?? null, + deadline: entity.deadline ?? null, + isActive: entity.isActive, + isPublic: entity.isPublic, + }; + } + + async save(entity: Vacancy): Promise { + const data = this.buildPrismaData(entity); + const { ownerId, id } = entity; + const vacancy = await this.prisma.vacancy.upsert({ + where: { id: entity.id }, + create: { + id, + ownerId, + ...data, + }, + update: data, + include: this.vacancyInclude, + }); + + return ( + this.vacancyMapper.toDomain(vacancy) ?? + raise("Failed to map vacancy to domain") + ); + } + + async destroy({ id }: Vacancy): Promise { + await this.prisma.vacancy.delete({ + where: { id }, + }); + } +} diff --git a/packages/system/src/base/base.entity.ts b/packages/core/src/shared/base.entity.ts similarity index 100% rename from packages/system/src/base/base.entity.ts rename to packages/core/src/shared/base.entity.ts diff --git a/packages/system/src/base/base.module.ts b/packages/core/src/shared/base.module.ts similarity index 100% rename from packages/system/src/base/base.module.ts rename to packages/core/src/shared/base.module.ts diff --git a/packages/system/src/base/clock.service.ts b/packages/core/src/shared/clock.service.ts similarity index 100% rename from packages/system/src/base/clock.service.ts rename to packages/core/src/shared/clock.service.ts diff --git a/packages/system/src/base/connection.types.ts b/packages/core/src/shared/connection.types.ts similarity index 100% rename from packages/system/src/base/connection.types.ts rename to packages/core/src/shared/connection.types.ts diff --git a/packages/system/src/base/create-gql-param-decorator.ts b/packages/core/src/shared/create-gql-param-decorator.ts similarity index 100% rename from packages/system/src/base/create-gql-param-decorator.ts rename to packages/core/src/shared/create-gql-param-decorator.ts diff --git a/packages/system/src/base/cursor.service.ts b/packages/core/src/shared/cursor.service.ts similarity index 100% rename from packages/system/src/base/cursor.service.ts rename to packages/core/src/shared/cursor.service.ts diff --git a/packages/system/src/base/dataloader.service.ts b/packages/core/src/shared/dataloader.service.ts similarity index 100% rename from packages/system/src/base/dataloader.service.ts rename to packages/core/src/shared/dataloader.service.ts diff --git a/packages/system/src/base/entity-service.interface.ts b/packages/core/src/shared/entity-service.interface.ts similarity index 100% rename from packages/system/src/base/entity-service.interface.ts rename to packages/core/src/shared/entity-service.interface.ts diff --git a/packages/system/src/errors/domain-error.ts b/packages/core/src/shared/errors/domain-error.ts similarity index 100% rename from packages/system/src/errors/domain-error.ts rename to packages/core/src/shared/errors/domain-error.ts diff --git a/packages/system/src/errors/index.ts b/packages/core/src/shared/errors/index.ts similarity index 100% rename from packages/system/src/errors/index.ts rename to packages/core/src/shared/errors/index.ts diff --git a/packages/system/src/base/factory.interface.ts b/packages/core/src/shared/factory.interface.ts similarity index 100% rename from packages/system/src/base/factory.interface.ts rename to packages/core/src/shared/factory.interface.ts diff --git a/packages/system/src/base/index.ts b/packages/core/src/shared/index.ts similarity index 94% rename from packages/system/src/base/index.ts rename to packages/core/src/shared/index.ts index 0188816..4a3bbaa 100644 --- a/packages/system/src/base/index.ts +++ b/packages/core/src/shared/index.ts @@ -6,7 +6,6 @@ export * from "./connection.types"; export * from "./cursor.service"; export * from "./dataloader.service"; export * from "./entity-service.interface"; -export * from "./event.interface"; export * from "./factory.interface"; export * from "./mapper.interface"; export * from "./named-entity"; @@ -17,3 +16,4 @@ export * from "./pagination.types"; export * from "./raise.util"; export * from "./unauthorized.util"; export * from "./uuid-factory.service"; +export * from "./errors"; diff --git a/packages/system/src/base/mapper.interface.ts b/packages/core/src/shared/mapper.interface.ts similarity index 100% rename from packages/system/src/base/mapper.interface.ts rename to packages/core/src/shared/mapper.interface.ts diff --git a/packages/system/src/base/named-entity.mapper.ts b/packages/core/src/shared/named-entity.mapper.ts similarity index 100% rename from packages/system/src/base/named-entity.mapper.ts rename to packages/core/src/shared/named-entity.mapper.ts diff --git a/packages/system/src/base/named-entity.service.ts b/packages/core/src/shared/named-entity.service.ts similarity index 98% rename from packages/system/src/base/named-entity.service.ts rename to packages/core/src/shared/named-entity.service.ts index 5b5d6aa..abde7ce 100644 --- a/packages/system/src/base/named-entity.service.ts +++ b/packages/core/src/shared/named-entity.service.ts @@ -1,4 +1,4 @@ -import { PrismaService } from "../database/prisma.service"; +import { PrismaService } from "../modules/database/prisma.service"; import type { EntityService } from "./entity-service.interface"; import type { BaseMapper } from "./mapper.interface"; import type { NamedEntity } from "./named-entity"; diff --git a/packages/system/src/base/named-entity.ts b/packages/core/src/shared/named-entity.ts similarity index 100% rename from packages/system/src/base/named-entity.ts rename to packages/core/src/shared/named-entity.ts diff --git a/packages/system/src/base/pagination.service.ts b/packages/core/src/shared/pagination.service.ts similarity index 100% rename from packages/system/src/base/pagination.service.ts rename to packages/core/src/shared/pagination.service.ts diff --git a/packages/system/src/base/pagination.types.ts b/packages/core/src/shared/pagination.types.ts similarity index 100% rename from packages/system/src/base/pagination.types.ts rename to packages/core/src/shared/pagination.types.ts diff --git a/packages/system/src/base/raise.util.ts b/packages/core/src/shared/raise.util.ts similarity index 100% rename from packages/system/src/base/raise.util.ts rename to packages/core/src/shared/raise.util.ts diff --git a/packages/system/src/base/unauthorized.util.ts b/packages/core/src/shared/unauthorized.util.ts similarity index 100% rename from packages/system/src/base/unauthorized.util.ts rename to packages/core/src/shared/unauthorized.util.ts diff --git a/packages/system/src/base/uuid-factory.service.ts b/packages/core/src/shared/uuid-factory.service.ts similarity index 100% rename from packages/system/src/base/uuid-factory.service.ts rename to packages/core/src/shared/uuid-factory.service.ts diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..b1f2ec3 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@cv/tsconfig/tsconfig.library.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "preserveSymlinks": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*", "../tsconfig/graphql.d.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/__tests__/**"] +} diff --git a/packages/file-storage/package.json b/packages/file-storage/package.json index e95932c..aa3786a 100644 --- a/packages/file-storage/package.json +++ b/packages/file-storage/package.json @@ -21,9 +21,9 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.800.0", - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7" + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18" }, "devDependencies": { "@biomejs/biome": "^2.2.6", @@ -32,8 +32,8 @@ "typescript": "^5.6.3" }, "peerDependencies": { - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7" + "@nestjs/common": "^11.1.18", + "@nestjs/config": "^4.0.4", + "@nestjs/core": "^11.1.18" } } diff --git a/packages/file-storage/src/file-storage.module.ts b/packages/file-storage/src/file-storage.module.ts index 4d948ee..d8f6872 100644 --- a/packages/file-storage/src/file-storage.module.ts +++ b/packages/file-storage/src/file-storage.module.ts @@ -15,7 +15,9 @@ export interface FileStorageConfigProvider { createFileStorageConfig(): FileStorageModuleConfig; } -export const FILE_STORAGE_CONFIG_PROVIDER = Symbol("FILE_STORAGE_CONFIG_PROVIDER"); +export const FILE_STORAGE_CONFIG_PROVIDER = Symbol( + "FILE_STORAGE_CONFIG_PROVIDER", +); @Module({}) export class FileStorageModule { diff --git a/packages/file-storage/src/index.ts b/packages/file-storage/src/index.ts index 63cffe2..e9df507 100644 --- a/packages/file-storage/src/index.ts +++ b/packages/file-storage/src/index.ts @@ -1,8 +1,8 @@ -export { FILE_STORAGE, type FileStorage } from "./file-storage.interface"; export { DiskFileStorage } from "./disk-file-storage"; -export { R2FileStorage, type R2FileStorageConfig } from "./r2-file-storage"; +export { FILE_STORAGE, type FileStorage } from "./file-storage.interface"; export { + type FileStorageConfigProvider, FileStorageModule, type FileStorageModuleConfig, - type FileStorageConfigProvider, } from "./file-storage.module"; +export { R2FileStorage, type R2FileStorageConfig } from "./r2-file-storage"; diff --git a/packages/file-storage/src/r2-file-storage.ts b/packages/file-storage/src/r2-file-storage.ts index f6c1f7a..8552717 100644 --- a/packages/file-storage/src/r2-file-storage.ts +++ b/packages/file-storage/src/r2-file-storage.ts @@ -1,4 +1,3 @@ -import { Injectable, Logger } from "@nestjs/common"; import { DeleteObjectCommand, GetObjectCommand, @@ -6,6 +5,7 @@ import { PutObjectCommand, S3Client, } from "@aws-sdk/client-s3"; +import { Injectable, Logger } from "@nestjs/common"; import type { FileStorage } from "./file-storage.interface"; export type R2FileStorageConfig = { @@ -41,7 +41,9 @@ export class R2FileStorage implements FileStorage { Body: data, }), ); - this.logger.debug(`Uploaded ${data.length} bytes to r2://${this.bucket}/${key}`); + this.logger.debug( + `Uploaded ${data.length} bytes to r2://${this.bucket}/${key}`, + ); } async read(key: string): Promise { diff --git a/packages/file-upload/jest.config.ts b/packages/file-upload/jest.config.ts deleted file mode 100644 index 000b319..0000000 --- a/packages/file-upload/jest.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Config } from "jest"; - -const config: Config = { - preset: "ts-jest", - testEnvironment: "node", - rootDir: ".", - moduleFileExtensions: ["ts", "tsx", "js", "json"], - testMatch: ["/src/**/*.spec.ts", "/src/**/*.test.ts"], - transform: { - "^.+\\.(t|j)sx?$": [ - "ts-jest", - { - diagnostics: false, - tsconfig: { - sourceMap: true, - inlineSourceMap: true, - }, - }, - ], - }, -}; - -export default config; diff --git a/packages/file-upload/package.json b/packages/file-upload/package.json index d709aaa..1e92687 100644 --- a/packages/file-upload/package.json +++ b/packages/file-upload/package.json @@ -6,7 +6,8 @@ "types": "./src/index.ts", "scripts": { "typecheck": "tsc --noEmit", - "test": "jest" + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@nestjs/common": "^11.1.3", @@ -23,11 +24,9 @@ }, "devDependencies": { "@cv/tsconfig": "*", - "@types/jest": "^29.5.0", "@types/node": "^22.7.5", "@types/pdf-parse": "^1.1.5", - "jest": "^29.7.0", - "ts-jest": "^29.1.1", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vitest": "^4.0.0" } } diff --git a/packages/file-upload/src/extractors/pdf/__tests__/pdf-extractor.spec.ts b/packages/file-upload/src/extractors/pdf/__tests__/pdf-extractor.spec.ts index 47109aa..bd6d85d 100644 --- a/packages/file-upload/src/extractors/pdf/__tests__/pdf-extractor.spec.ts +++ b/packages/file-upload/src/extractors/pdf/__tests__/pdf-extractor.spec.ts @@ -1,3 +1,4 @@ +import type { Mock } from 'vitest'; import type { TextExtractionResult } from '../../../types'; import type { PdfExtractionStrategy } from '../types'; import { PDFExtractor } from '../pdf.extractor'; @@ -6,10 +7,10 @@ const makeStrategy = ( name: string, priority: number, result: TextExtractionResult, -): PdfExtractionStrategy & { extract: jest.Mock } => ({ +): PdfExtractionStrategy & { extract: Mock } => ({ name, priority, - extract: jest.fn().mockResolvedValue(result), + extract: vi.fn().mockResolvedValue(result), }); const buf = Buffer.from('fake-pdf'); diff --git a/packages/file-upload/vitest.config.ts b/packages/file-upload/vitest.config.ts new file mode 100644 index 0000000..5751952 --- /dev/null +++ b/packages/file-upload/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + root: "./src", + globals: true, + }, +}); diff --git a/packages/handlers/package.json b/packages/handlers/package.json new file mode 100644 index 0000000..2640cfa --- /dev/null +++ b/packages/handlers/package.json @@ -0,0 +1,24 @@ +{ + "name": "@cv/handlers", + "version": "0.0.0", + "private": true, + "main": "./src/index.ts", + "types": "./src/index.ts", + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@cv/core": "workspace:*", + "@cv/file-storage": "workspace:*", + "@nestjs/common": "^11.1.18", + "@riotbyte-com/project-q-core": "1.0.19-rc.3", + "@riotbyte-com/project-q-nestjs": "1.0.19-rc.3", + "playwright": "^1.52.0", + "playwright-core": "^1.52.0" + }, + "devDependencies": { + "@cv/tsconfig": "*", + "@types/node": "^22.7.5", + "typescript": "^5.3.3" + } +} diff --git a/packages/handlers/src/handlers.module.ts b/packages/handlers/src/handlers.module.ts new file mode 100644 index 0000000..73ffab2 --- /dev/null +++ b/packages/handlers/src/handlers.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { HtmlToPdfService } from "./pdf/html-to-pdf.service"; +import { RenderPdfHandler } from "./render-pdf.handler"; + +export const PDF_SERVICE_CONFIG = Symbol("PDF_SERVICE_CONFIG"); + +@Module({ + providers: [RenderPdfHandler, HtmlToPdfService], + exports: [RenderPdfHandler, HtmlToPdfService], +}) +export class HandlersModule {} diff --git a/packages/handlers/src/index.ts b/packages/handlers/src/index.ts new file mode 100644 index 0000000..5a49942 --- /dev/null +++ b/packages/handlers/src/index.ts @@ -0,0 +1,3 @@ +export { HandlersModule } from "./handlers.module"; +export { RenderPdfHandler } from "./render-pdf.handler"; +export { HtmlToPdfService, type PdfServiceConfig } from "./pdf/html-to-pdf.service"; diff --git a/apps/worker/src/pdf/html-to-pdf.service.ts b/packages/handlers/src/pdf/html-to-pdf.service.ts similarity index 51% rename from apps/worker/src/pdf/html-to-pdf.service.ts rename to packages/handlers/src/pdf/html-to-pdf.service.ts index 2995f49..7108e63 100644 --- a/apps/worker/src/pdf/html-to-pdf.service.ts +++ b/packages/handlers/src/pdf/html-to-pdf.service.ts @@ -1,44 +1,43 @@ -import { Inject, Injectable, Logger, OnModuleDestroy } from "@nestjs/common"; +import { Injectable, Logger, OnModuleDestroy } from "@nestjs/common"; import { chromium } from "playwright"; import type { Browser } from "playwright-core"; -import type { WorkerConfig } from "../config"; -import { WORKER_CONFIG } from "../worker.module"; -/** Manages a Playwright browser singleton for HTML-to-PDF conversion. */ +export interface PdfServiceConfig { + chromiumArgs: readonly string[]; + pdfTimeoutMs: number; +} + +/** + * Manages a Playwright browser singleton for HTML-to-PDF conversion. + */ @Injectable() export class HtmlToPdfService implements OnModuleDestroy { private readonly logger = new Logger(HtmlToPdfService.name); private browser: Browser | null = null; + private readonly config: PdfServiceConfig; - constructor( - @Inject(WORKER_CONFIG) private readonly config: WorkerConfig, - ) {} + constructor(config: PdfServiceConfig) { + this.config = config; + } private async ensureBrowser(): Promise { if (this.browser) { return this.browser; } - this.logger.log( - `Launching Chromium with args: ${this.config.chromiumArgs.join(" ")}`, - ); - this.browser = await chromium.launch({ args: this.config.chromiumArgs }); - + this.logger.log(`Launching Chromium with args: ${this.config.chromiumArgs.join(" ")}`); + this.browser = await chromium.launch({ args: [...this.config.chromiumArgs] }); return this.browser; } - /** Renders HTML to an A4 PDF buffer. */ - async convert(html: string, timeoutMs = 30_000): Promise { + async convert(html: string, timeoutMs?: number): Promise { + const timeout = timeoutMs ?? this.config.pdfTimeoutMs; const browser = await this.ensureBrowser(); const page = await browser.newPage(); - page.setDefaultTimeout(timeoutMs); + page.setDefaultTimeout(timeout); try { - await page.setContent(html, { - waitUntil: "networkidle", - timeout: timeoutMs, - }); - + await page.setContent(html, { waitUntil: "networkidle", timeout }); return await page.pdf({ format: "A4", printBackground: true }); } finally { await page.close(); diff --git a/apps/worker/src/handlers/render-pdf.handler.ts b/packages/handlers/src/render-pdf.handler.ts similarity index 68% rename from apps/worker/src/handlers/render-pdf.handler.ts rename to packages/handlers/src/render-pdf.handler.ts index 776df1e..f93584b 100644 --- a/apps/worker/src/handlers/render-pdf.handler.ts +++ b/packages/handlers/src/render-pdf.handler.ts @@ -1,16 +1,14 @@ +import { FILE_STORAGE, type FileStorage } from "@cv/file-storage"; import { Inject, Injectable, Logger } from "@nestjs/common"; import type { Envelope, Handler } from "@riotbyte-com/project-q-core"; import { HandlerTag } from "@riotbyte-com/project-q-nestjs"; -import { FILE_STORAGE, type FileStorage } from "@cv/file-storage"; -import type { WorkerConfig } from "../config"; -import { HtmlToPdfService } from "../pdf/html-to-pdf.service"; -import { WORKER_CONFIG } from "../worker.module"; +import { HtmlToPdfService } from "./pdf/html-to-pdf.service"; -type RenderPdfData = { +interface RenderPdfData { cvId: string; html: string; requestedBy: string; -}; +} @Injectable() @HandlerTag.decorator({ handles: "render-pdf" }) @@ -19,19 +17,14 @@ export class RenderPdfHandler implements Handler { constructor( private readonly pdfService: HtmlToPdfService, - @Inject(WORKER_CONFIG) private readonly config: WorkerConfig, @Inject(FILE_STORAGE) private readonly storage: FileStorage, ) {} async handle(envelope: Envelope): Promise { const { cvId, html, requestedBy } = envelope.message.data as RenderPdfData; + this.logger.log(`Rendering PDF for CV ${cvId} (requested by ${requestedBy})`); - this.logger.log( - `Rendering PDF for CV ${cvId} (requested by ${requestedBy})`, - ); - - const pdf = await this.pdfService.convert(html, this.config.pdfTimeoutMs); - + const pdf = await this.pdfService.convert(html); const key = `${cvId}.pdf`; await this.storage.write(key, pdf); diff --git a/packages/handlers/tsconfig.json b/packages/handlers/tsconfig.json new file mode 100644 index 0000000..7c1285e --- /dev/null +++ b/packages/handlers/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@cv/tsconfig/tsconfig.library.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/packages/system/README.md b/packages/system/README.md deleted file mode 100644 index 1d59c3c..0000000 --- a/packages/system/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# @cv/system - -Infrastructure and foundation package for the CV Generator monorepo. - -## Overview - -This package provides reusable infrastructure services and utilities that form the foundation layer of the application. It contains database services, mail services, base entities, and domain error classes. - -## Package Structure - -``` -src/ -├── database/ # Database services -│ ├── prisma.service.ts -│ └── database.module.ts -├── base/ # Base utilities -│ ├── base.entity.ts -│ ├── pagination.service.ts -│ ├── cursor.service.ts -│ ├── raise.util.ts -│ └── unauthorized.util.ts -├── mail/ # Email services -│ ├── mail.module.ts -│ ├── resend/ # Resend implementation -│ └── template/ # Email templating -├── errors/ # Domain errors -│ └── domain-error.ts -└── index.ts -``` - -## Usage - -### Installation - -This package is part of the monorepo and is automatically available to other packages via npm workspaces. - -### Importing - -```typescript -import { PrismaService, DatabaseModule, BaseEntity } from "@cv/system"; -``` - -### Key Exports - -- **Modules**: `DatabaseModule`, `ResendModule`, `TemplateModule`, `BaseModule` -- **Services**: `PrismaService`, `MailService`, `HandlebarsTemplateService`, `TemplateRegistryService` -- **Entities**: `BaseEntity` -- **Utilities**: `raise`, `unauthorized`, `PaginationService`, `CursorService` -- **Errors**: `DomainError` - -## Dependencies - -- **Peer Dependencies**: `@nestjs/common`, `@nestjs/core`, `@prisma/client`, `resend`, `handlebars` -- **Internal Dependencies**: None (foundation layer) - -## Architecture - -This package is the foundation layer and has no dependencies on other monorepo packages. It provides: -- Database access via Prisma -- Email sending and templating -- Base entity classes -- Pagination and cursor utilities -- Domain error base classes - -All packages in the monorepo depend on this package, but this package depends on nothing within the monorepo. - diff --git a/packages/system/package.json b/packages/system/package.json deleted file mode 100644 index 0870ae2..0000000 --- a/packages/system/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@cv/system", - "version": "0.0.0", - "private": true, - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": { - "require": "./src/index.ts", - "import": "./src/index.ts", - "types": "./src/index.ts" - } - }, - "files": [ - "src/" - ], - "scripts": { - "lint": "biome check .", - "lint:fix": "biome check --write .", - "typecheck": "tsc -b" - }, - "dependencies": { - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", - "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@prisma/adapter-pg": "^7.1.0", - "@prisma/client": "^7.1.0", - "dataloader": "^2.2.3", - "graphql": "^16.12.0", - "handlebars": "^4.7.8", - "pg": "^8.16.3", - "reflect-metadata": "^0.2.2", - "resend": "^6.5.2" - }, - "devDependencies": { - "@biomejs/biome": "^2.2.6", - "@cv/biome-config": "*", - "@cv/tsconfig": "*", - "@types/pg": "^8.15.0", - "typescript": "^5.6.3" - }, - "peerDependencies": { - "@nestjs/common": "^10.4.7", - "@nestjs/config": "^3.2.0", - "@nestjs/core": "^10.4.7", - "@nestjs/event-emitter": "^3.0.1", - "@nestjs/graphql": "^12.2.2", - "@prisma/adapter-pg": "^7.1.0", - "@prisma/client": "^7.1.0", - "graphql": "^16.12.0", - "handlebars": "^4.7.8", - "pg": "^8.16.3", - "resend": "^6.5.2" - } -} diff --git a/packages/system/src/base/event.interface.ts b/packages/system/src/base/event.interface.ts deleted file mode 100644 index d47260a..0000000 --- a/packages/system/src/base/event.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Event { - readonly eventName: symbol; -} diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts deleted file mode 100644 index 4402fa0..0000000 --- a/packages/system/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./base"; -export * from "./database"; -export * from "./errors"; -export * from "./mail"; diff --git a/packages/system/tsconfig.json b/packages/system/tsconfig.json deleted file mode 100644 index 25dc8ba..0000000 --- a/packages/system/tsconfig.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "@cv/tsconfig/tsconfig.library.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "baseUrl": ".", - "composite": true, - "declaration": true, - "emitDeclarationOnly": true, - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*"] -} diff --git a/packages/ui/src/components/Progress.tsx b/packages/ui/src/components/Progress.tsx index a1b2a19..cecf8f0 100644 --- a/packages/ui/src/components/Progress.tsx +++ b/packages/ui/src/components/Progress.tsx @@ -67,20 +67,23 @@ const getStepPropsFromElement = (element: ReactElement): StepProps | null => { return validateStepProps(element.props); }; -const stepContainerVariants = cva("flex items-center space-x-2 transition-colors duration-200", { - variants: { - state: { - selected: "text-ctp-blue", - completed: "text-ctp-green", - pending: "text-ctp-subtext0", - error: "text-ctp-red", - disabled: "text-ctp-overlay0 opacity-50", +const stepContainerVariants = cva( + "flex items-center space-x-2 transition-colors duration-200", + { + variants: { + state: { + selected: "text-ctp-blue", + completed: "text-ctp-green", + pending: "text-ctp-subtext0", + error: "text-ctp-red", + disabled: "text-ctp-overlay0 opacity-50", + }, + }, + defaultVariants: { + state: "pending", }, }, - defaultVariants: { - state: "pending", - }, -}); +); const stepIndicatorVariants = cva( "h-8 w-8 rounded-full flex items-center justify-center transition-colors duration-200", diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 7ae6193..5fdd69f 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -71,3 +71,20 @@ export const range = (length: number): number[] => */ export const ensureArray = (value: T | T[]): T[] => Array.isArray(value) ? value : [value]; + +/** + * Returns a new object with all `null` and `undefined` values removed. + * Useful for building Prisma inputs where optional fields must be absent rather than undefined. + * + * @example + * ```typescript + * compact({ a: 1, b: undefined, c: null, d: "hi" }) + * // → { a: 1, d: "hi" } + * ``` + */ +export const compact = >( + obj: T, +): { [K in keyof T as T[K] extends null | undefined ? never : K]: NonNullable } => + Object.fromEntries( + Object.entries(obj).filter(([, v]) => v != null), + ) as never; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69ea425..36cc9f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,202 +28,14 @@ importers: specifier: ^8.2.4 version: 8.2.4(@swc/core@1.15.13)(@types/node@22.19.3)(encoding@0.1.13) - apps/client: - dependencies: - '@cv/routing': - specifier: '*' - version: link:../../packages/routing - '@cv/ui': - specifier: '*' - version: link:../../packages/ui - '@cv/utils': - specifier: '*' - version: link:../../packages/utils - '@hookform/resolvers': - specifier: ^5.2.2 - version: 5.2.2(react-hook-form@7.70.0(react@19.2.3)) - '@tanstack/react-query': - specifier: ^5.59.0 - version: 5.90.16(react@19.2.3) - '@tanstack/react-query-devtools': - specifier: ^5.59.0 - version: 5.91.2(@tanstack/react-query@5.90.16(react@19.2.3))(react@19.2.3) - '@tanstack/react-virtual': - specifier: ^3.13.12 - version: 3.13.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@types/react-router-dom': - specifier: ^5.3.3 - version: 5.3.3 - class-variance-authority: - specifier: ^0.7.1 - version: 0.7.1 - clsx: - specifier: ^2.0.0 - version: 2.1.1 - graphql: - specifier: ^16.12.0 - version: 16.12.0 - graphql-request: - specifier: ^6.1.0 - version: 6.1.0(encoding@0.1.13)(graphql@16.12.0) - graphql-type-json: - specifier: ^0.3.2 - version: 0.3.2(graphql@16.12.0) - react: - specifier: ^19.2.3 - version: 19.2.3 - react-dom: - specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) - react-dropzone: - specifier: ^14.3.8 - version: 14.3.8(react@19.2.3) - react-hook-form: - specifier: ^7.70.0 - version: 7.70.0(react@19.2.3) - react-router-dom: - specifier: ^7.9.4 - version: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - tailwind-merge: - specifier: ^2.0.0 - version: 2.6.0 - zod: - specifier: ^4.3.6 - version: 4.3.6 - devDependencies: - '@biomejs/biome': - specifier: ^2.2.6 - version: 2.3.11 - '@cv/biome-config': - specifier: '*' - version: link:../../packages/biome-config - '@cv/tsconfig': - specifier: '*' - version: link:../../packages/tsconfig - '@graphql-codegen/cli': - specifier: ^6.1.0 - version: 6.1.0(@parcel/watcher@2.5.1)(@types/node@22.19.3)(encoding@0.1.13)(graphql@16.12.0)(typescript@5.9.3) - '@graphql-codegen/typescript': - specifier: ^5.0.6 - version: 5.0.7(encoding@0.1.13)(graphql@16.12.0) - '@graphql-codegen/typescript-operations': - specifier: ^5.0.6 - version: 5.0.7(encoding@0.1.13)(graphql@16.12.0) - '@graphql-codegen/typescript-react-query': - specifier: ^6.1.0 - version: 6.1.1(encoding@0.1.13)(graphql@16.12.0) - '@tailwindcss/postcss': - specifier: ^4.1.15 - version: 4.1.18 - '@tailwindcss/vite': - specifier: ^4.0.0 - version: 4.1.18(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) - '@types/react': - specifier: ^19.0.6 - version: 19.2.7 - '@types/react-dom': - specifier: ^19.0.3 - version: 19.2.3(@types/react@19.2.7) - '@vitejs/plugin-react': - specifier: ^4.3.4 - version: 4.7.0(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) - tailwindcss: - specifier: ^4.0.0 - version: 4.1.18 - typescript: - specifier: ^5.6.3 - version: 5.9.3 - vite: - specifier: ^7.1.10 - version: 7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) - - apps/docs: - dependencies: - '@catppuccin/palette': - specifier: ^1.4.0 - version: 1.7.1 - '@cv/routing': - specifier: '*' - version: link:../../packages/routing - '@cv/ui': - specifier: '*' - version: link:../../packages/ui - '@cv/utils': - specifier: '*' - version: link:../../packages/utils - '@mdx-js/react': - specifier: ^3.1.1 - version: 3.1.1(@types/react@19.2.7)(react@19.2.3) - '@mdx-js/rollup': - specifier: ^3.1.1 - version: 3.1.1(rollup@4.54.0) - '@types/mdx': - specifier: ^2.0.13 - version: 2.0.13 - highlight.js: - specifier: ^11.10.0 - version: 11.11.1 - react: - specifier: ^19.2.3 - version: 19.2.3 - react-dom: - specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) - react-markdown: - specifier: ^9.0.1 - version: 9.1.0(@types/react@19.2.7)(react@19.2.3) - react-router-dom: - specifier: ^6.26.1 - version: 6.30.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - rehype-highlight: - specifier: ^7.0.0 - version: 7.0.2 - remark-gfm: - specifier: ^4.0.0 - version: 4.0.1 - devDependencies: - '@tailwindcss/postcss': - specifier: ^4.0.0 - version: 4.1.18 - '@tailwindcss/vite': - specifier: ^4.0.0 - version: 4.1.18(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2)) - '@types/mdast': - specifier: ^4.0.4 - version: 4.0.4 - '@types/react': - specifier: ^19.0.6 - version: 19.2.7 - '@types/react-dom': - specifier: ^19.0.3 - version: 19.2.3(@types/react@19.2.7) - '@types/unist': - specifier: ^3.0.3 - version: 3.0.3 - '@vitejs/plugin-react': - specifier: ^4.3.1 - version: 4.7.0(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2)) - autoprefixer: - specifier: ^10.4.20 - version: 10.4.23(postcss@8.5.6) - postcss: - specifier: ^8.4.47 - version: 8.5.6 - tailwindcss: - specifier: ^4.0.0 - version: 4.1.18 - typescript: - specifier: ^5.5.3 - version: 5.9.3 - vite: - specifier: ^5.4.3 - version: 5.4.21(@types/node@22.19.3)(lightningcss@1.30.2) - - apps/server: + apps/api: dependencies: '@apollo/server': - specifier: ^4.11.0 - version: 4.12.2(encoding@0.1.13)(graphql@16.12.0) + specifier: ^5.0.0 + version: 5.5.0(graphql@16.12.0) + '@as-integrations/express5': + specifier: ^1.1.2 + version: 1.1.2(@apollo/server@5.5.0(graphql@16.12.0))(express@5.2.1) '@cv/ai-parser': specifier: '*' version: link:../../packages/ai-parser @@ -231,8 +43,11 @@ importers: specifier: '*' version: link:../../packages/ai-provider '@cv/auth': - specifier: '*' + specifier: workspace:* version: link:../../packages/auth + '@cv/core': + specifier: workspace:* + version: link:../../packages/core '@cv/cv-renderer': specifier: '*' version: link:../../packages/cv-renderer @@ -242,42 +57,39 @@ importers: '@cv/file-upload': specifier: '*' version: link:../../packages/file-upload - '@cv/system': - specifier: '*' - version: link:../../packages/system '@cv/utils': specifier: '*' version: link:../../packages/utils '@nestjs/apollo': - specifier: ^12.2.2 - version: 12.2.2(@apollo/server@4.12.2(encoding@0.1.13)(graphql@16.12.0))(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/graphql@12.2.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2))(graphql@16.12.0) + specifier: ^13.2.5 + version: 13.2.5(@apollo/server@5.5.0(graphql@16.12.0))(@as-integrations/express5@1.1.2(@apollo/server@5.5.0(graphql@16.12.0))(express@5.2.1))(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/graphql@13.2.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2))(graphql@16.12.0) '@nestjs/common': - specifier: ^10.4.7 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': - specifier: ^3.2.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + specifier: ^4.0.4 + version: 4.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^10.4.7 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/event-emitter': specifier: ^3.0.1 - version: 3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + version: 3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@nestjs/graphql': - specifier: ^12.2.2 - version: 12.2.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2) + specifier: ^13.2.5 + version: 13.2.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2) '@nestjs/jwt': - specifier: ^10.2.0 - version: 10.2.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + specifier: ^11.0.2 + version: 11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/mapped-types': specifier: ^2.1.0 - version: 2.1.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + version: 2.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) '@nestjs/passport': - specifier: ^10.0.3 - version: 10.0.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) '@nestjs/platform-express': - specifier: ^10.4.7 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@prisma/adapter-pg': specifier: ^7.1.0 version: 7.2.0 @@ -286,16 +98,16 @@ importers: version: 7.2.0(prisma@7.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3) '@riotbyte-com/nest-service-locator': specifier: 0.3.0-rc.4 - version: 0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + version: 0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@riotbyte-com/project-q-core': specifier: 1.0.19-rc.3 version: 1.0.19-rc.3 '@riotbyte-com/project-q-nestjs': specifier: 1.0.19-rc.3 - version: 1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3)) + version: 1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3)) '@riotbyte-com/project-q-prisma': specifier: 1.0.19-rc.3 - version: 1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@riotbyte-com/project-q-core@1.0.19-rc.3)(@riotbyte-com/project-q-nestjs@1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3))) + version: 1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@riotbyte-com/project-q-core@1.0.19-rc.3)(@riotbyte-com/project-q-nestjs@1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3))) '@types/cookie-parser': specifier: ^1.4.10 version: 1.4.10(@types/express@5.0.6) @@ -305,9 +117,6 @@ importers: '@types/multer': specifier: ^2.0.0 version: 2.0.0 - apollo-server-express: - specifier: ^3.13.0 - version: 3.13.0(encoding@0.1.13)(express@4.21.2)(graphql@16.12.0) bcryptjs: specifier: ^2.4.3 version: 2.4.3 @@ -327,8 +136,8 @@ importers: specifier: ^2.2.3 version: 2.2.3 express: - specifier: ^4.21.2 - version: 4.21.2 + specifier: ^5.1.0 + version: 5.2.1 graphql: specifier: ^16.12.0 version: 16.12.0 @@ -346,13 +155,13 @@ importers: version: 1.11.0 multer: specifier: ^2.0.2 - version: 2.0.2 + version: 2.1.1 nest-commander: specifier: ^3.16.0 - version: 3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3) + version: 3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3) nestjs-zod: specifier: ^5.1.1 - version: 5.1.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)(zod@4.3.6) + version: 5.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)(zod@4.3.6) passport: specifier: ^0.7.0 version: 0.7.0 @@ -390,6 +199,9 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: + '@aws-sdk/client-s3': + specifier: ^3.800.0 + version: 3.1013.0 '@biomejs/biome': specifier: ^2.2.6 version: 2.3.11 @@ -403,8 +215,8 @@ importers: specifier: ^9.9.0 version: 9.9.0 '@nestjs/testing': - specifier: ^10.4.7 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@10.4.20) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-express@11.1.18) '@swc/core': specifier: ^1.15.13 version: 1.15.13 @@ -414,9 +226,9 @@ importers: '@types/express': specifier: ^5.0.0 version: 5.0.6 - '@types/jest': - specifier: ^29.5.12 - version: 29.5.14 + '@types/express-serve-static-core': + specifier: ^5.0.0 + version: 5.1.0 '@types/node': specifier: ^22.7.5 version: 22.19.3 @@ -435,9 +247,6 @@ importers: '@vitest/ui': specifier: ^4.0.16 version: 4.0.16(vitest@4.0.16) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) nodemon: specifier: ^3.1.7 version: 3.1.11 @@ -447,9 +256,6 @@ importers: supertest: specifier: ^6.3.4 version: 6.3.4 - ts-jest: - specifier: ^29.1.2 - version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3) @@ -463,50 +269,247 @@ importers: specifier: ^8.5.1 version: 8.5.1(@swc/core@1.15.13)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) typescript: - specifier: ^5.6.3 + specifier: ^5.6.3 + version: 5.9.3 + vitest: + specifier: ^4.0.16 + version: 4.0.16(@types/node@22.19.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + + apps/client: + dependencies: + '@cv/routing': + specifier: '*' + version: link:../../packages/routing + '@cv/ui': + specifier: '*' + version: link:../../packages/ui + '@cv/utils': + specifier: '*' + version: link:../../packages/utils + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.70.0(react@19.2.3)) + '@tanstack/react-query': + specifier: ^5.59.0 + version: 5.90.16(react@19.2.3) + '@tanstack/react-query-devtools': + specifier: ^5.59.0 + version: 5.91.2(@tanstack/react-query@5.90.16(react@19.2.3))(react@19.2.3) + '@tanstack/react-virtual': + specifier: ^3.13.12 + version: 3.13.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.0.0 + version: 2.1.1 + graphql: + specifier: ^16.12.0 + version: 16.12.0 + graphql-request: + specifier: ^6.1.0 + version: 6.1.0(encoding@0.1.13)(graphql@16.12.0) + graphql-type-json: + specifier: ^0.3.2 + version: 0.3.2(graphql@16.12.0) + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@19.2.3) + react-hook-form: + specifier: ^7.70.0 + version: 7.70.0(react@19.2.3) + react-router-dom: + specifier: ^7.9.4 + version: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + tailwind-merge: + specifier: ^2.0.0 + version: 2.6.0 + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@biomejs/biome': + specifier: ^2.2.6 + version: 2.3.11 + '@cv/biome-config': + specifier: '*' + version: link:../../packages/biome-config + '@cv/tsconfig': + specifier: '*' + version: link:../../packages/tsconfig + '@graphql-codegen/cli': + specifier: ^6.1.0 + version: 6.1.0(@parcel/watcher@2.5.1)(@types/node@22.19.3)(encoding@0.1.13)(graphql@16.12.0)(typescript@5.9.3) + '@graphql-codegen/typescript': + specifier: ^5.0.6 + version: 5.0.7(encoding@0.1.13)(graphql@16.12.0) + '@graphql-codegen/typescript-operations': + specifier: ^5.0.6 + version: 5.0.7(encoding@0.1.13)(graphql@16.12.0) + '@graphql-codegen/typescript-react-query': + specifier: ^6.1.0 + version: 6.1.1(encoding@0.1.13)(graphql@16.12.0) + '@tailwindcss/postcss': + specifier: ^4.1.15 + version: 4.1.18 + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.1.18(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + '@types/react': + specifier: ^19.0.6 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.2.3(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + tailwindcss: + specifier: ^4.0.0 + version: 4.1.18 + typescript: + specifier: ^5.6.3 + version: 5.9.3 + vite: + specifier: ^7.1.10 + version: 7.3.0(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + + apps/docs: + dependencies: + '@catppuccin/palette': + specifier: ^1.4.0 + version: 1.7.1 + '@cv/routing': + specifier: '*' + version: link:../../packages/routing + '@cv/ui': + specifier: '*' + version: link:../../packages/ui + '@cv/utils': + specifier: '*' + version: link:../../packages/utils + '@mdx-js/react': + specifier: ^3.1.1 + version: 3.1.1(@types/react@19.2.7)(react@19.2.3) + '@mdx-js/rollup': + specifier: ^3.1.1 + version: 3.1.1(rollup@4.54.0) + '@types/mdx': + specifier: ^2.0.13 + version: 2.0.13 + highlight.js: + specifier: ^11.10.0 + version: 11.11.1 + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + react-markdown: + specifier: ^9.0.1 + version: 9.1.0(@types/react@19.2.7)(react@19.2.3) + react-router-dom: + specifier: ^6.26.1 + version: 6.30.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + rehype-highlight: + specifier: ^7.0.0 + version: 7.0.2 + remark-gfm: + specifier: ^4.0.0 + version: 4.0.1 + devDependencies: + '@tailwindcss/postcss': + specifier: ^4.0.0 + version: 4.1.18 + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.1.18(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2)) + '@types/mdast': + specifier: ^4.0.4 + version: 4.0.4 + '@types/react': + specifier: ^19.0.6 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.2.3(@types/react@19.2.7) + '@types/unist': + specifier: ^3.0.3 + version: 3.0.3 + '@vitejs/plugin-react': + specifier: ^4.3.1 + version: 4.7.0(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2)) + autoprefixer: + specifier: ^10.4.20 + version: 10.4.23(postcss@8.5.6) + postcss: + specifier: ^8.4.47 + version: 8.5.6 + tailwindcss: + specifier: ^4.0.0 + version: 4.1.18 + typescript: + specifier: ^5.5.3 version: 5.9.3 - vitest: - specifier: ^4.0.16 - version: 4.0.16(@types/node@22.19.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + unified: + specifier: ^11.0.5 + version: 11.0.5 + vite: + specifier: ^5.4.3 + version: 5.4.21(@types/node@22.19.3)(lightningcss@1.30.2) apps/worker: dependencies: + '@cv/core': + specifier: workspace:* + version: link:../../packages/core '@cv/file-storage': specifier: '*' version: link:../../packages/file-storage - '@cv/system': - specifier: '*' - version: link:../../packages/system + '@cv/handlers': + specifier: workspace:* + version: link:../../packages/handlers '@nestjs/common': - specifier: ^10.4.7 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': - specifier: ^3.2.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + specifier: ^4.0.4 + version: 4.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^10.4.7 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/event-emitter': specifier: ^3.0.1 - version: 3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + version: 3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@riotbyte-com/nest-service-locator': specifier: 0.3.0-rc.4 - version: 0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + version: 0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@riotbyte-com/project-q-core': specifier: 1.0.19-rc.3 version: 1.0.19-rc.3 '@riotbyte-com/project-q-nestjs': specifier: 1.0.19-rc.3 - version: 1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3)) + version: 1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3)) '@riotbyte-com/project-q-prisma': specifier: 1.0.19-rc.3 - version: 1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@riotbyte-com/project-q-core@1.0.19-rc.3)(@riotbyte-com/project-q-nestjs@1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3))) + version: 1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@riotbyte-com/project-q-core@1.0.19-rc.3)(@riotbyte-com/project-q-nestjs@1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3))) eventemitter2: specifier: ^6.4.9 version: 6.4.9 nest-commander: specifier: ^3.16.0 - version: 3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3) + version: 3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3) playwright: specifier: ^1.52.0 version: 1.58.2 @@ -523,6 +526,9 @@ importers: specifier: ^4.3.6 version: 4.3.6 devDependencies: + '@aws-sdk/client-s3': + specifier: ^3.800.0 + version: 3.1013.0 '@cv/tsconfig': specifier: '*' version: link:../../packages/tsconfig @@ -532,6 +538,9 @@ importers: '@types/node': specifier: ^22.7.5 version: 22.19.3 + '@types/pg': + specifier: ^8.15.0 + version: 8.16.0 nodemon: specifier: ^3.1.7 version: 3.1.11 @@ -560,21 +569,15 @@ importers: '@cv/tsconfig': specifier: '*' version: link:../tsconfig - '@types/jest': - specifier: ^29.5.0 - version: 29.5.14 '@types/node': specifier: ^22.7.5 version: 22.19.3 - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - ts-jest: - specifier: ^29.1.1 - version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 + vitest: + specifier: ^4.0.0 + version: 4.0.16(@types/node@22.19.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) packages/ai-provider: dependencies: @@ -591,69 +594,201 @@ importers: '@cv/tsconfig': specifier: '*' version: link:../tsconfig - '@types/jest': - specifier: ^29.5.12 - version: 29.5.14 '@types/node': specifier: ^22.7.5 version: 22.19.3 - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - ts-jest: - specifier: ^29.1.2 - version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 + vitest: + specifier: ^4.0.0 + version: 4.0.16(@types/node@22.19.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) packages/auth: dependencies: - '@cv/system': + '@cv/core': + specifier: workspace:* + version: link:../core + '@nestjs/common': + specifier: ^11.1.18 + version: 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.4 + version: 4.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/graphql': + specifier: ^13.2.5 + version: 13.2.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2) + '@nestjs/jwt': + specifier: ^11.0.2 + version: 11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/passport': + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + express: + specifier: ^5.1.0 + version: 5.2.1 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + devDependencies: + '@cv/tsconfig': specifier: '*' - version: link:../system + version: link:../tsconfig + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/biome-config: {} + + packages/core: + dependencies: + '@cv/ai-parser': + specifier: workspace:* + version: link:../ai-parser + '@cv/ai-provider': + specifier: workspace:* + version: link:../ai-provider + '@cv/cv-renderer': + specifier: workspace:* + version: link:../cv-renderer + '@cv/file-storage': + specifier: workspace:* + version: link:../file-storage + '@cv/file-upload': + specifier: workspace:* + version: link:../file-upload + '@cv/utils': + specifier: workspace:* + version: link:../utils '@nestjs/common': - specifier: ^10.4.7 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': - specifier: ^3.2.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + specifier: ^4.0.4 + version: 4.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^10.4.7 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/event-emitter': specifier: ^3.0.1 - version: 3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + version: 3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@nestjs/graphql': - specifier: ^12.2.2 - version: 12.2.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2) + specifier: ^13.2.5 + version: 13.2.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2) '@nestjs/jwt': - specifier: ^10.2.0 - version: 10.2.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + specifier: ^11.0.2 + version: 11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/passport': + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@nestjs/platform-express': + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@prisma/adapter-pg': + specifier: ^7.1.0 + version: 7.2.0 + '@prisma/client': + specifier: ^7.1.0 + version: 7.2.0(prisma@7.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3) + '@riotbyte-com/nest-service-locator': + specifier: 0.3.0-rc.4 + version: 0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@riotbyte-com/project-q-core': + specifier: 1.0.19-rc.3 + version: 1.0.19-rc.3 + '@riotbyte-com/project-q-nestjs': + specifier: 1.0.19-rc.3 + version: 1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3)) + '@riotbyte-com/project-q-prisma': + specifier: 1.0.19-rc.3 + version: 1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@riotbyte-com/project-q-core@1.0.19-rc.3)(@riotbyte-com/project-q-nestjs@1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3))) bcryptjs: specifier: ^2.4.3 version: 2.4.3 + canvas: + specifier: ^3.1.0 + version: 3.2.1 + dataloader: + specifier: ^2.2.3 + version: 2.2.3 + express: + specifier: ^5.1.0 + version: 5.2.1 graphql: specifier: ^16.12.0 version: 16.12.0 + handlebars: + specifier: ^4.7.8 + version: 4.7.8 + mammoth: + specifier: ^1.11.0 + version: 1.11.0 + multer: + specifier: ^2.0.2 + version: 2.1.1 + nest-commander: + specifier: ^3.16.0 + version: 3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3) + nestjs-zod: + specifier: ^5.1.1 + version: 5.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)(zod@4.3.6) + neverthrow: + specifier: ^8.2.0 + version: 8.2.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + passport-local: + specifier: ^1.0.0 + version: 1.0.0 + pdf-parse: + specifier: ^1.1.4 + version: 1.1.4 + pg: + specifier: ^8.16.3 + version: 8.16.3 reflect-metadata: specifier: ^0.2.2 version: 0.2.2 + resend: + specifier: ^6.5.2 + version: 6.6.0 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + turndown: + specifier: ^7.2.0 + version: 7.2.4 zod: specifier: ^4.3.6 version: 4.3.6 devDependencies: - '@biomejs/biome': - specifier: ^2.2.6 - version: 2.3.11 - '@cv/biome-config': - specifier: '*' - version: link:../biome-config '@cv/tsconfig': specifier: '*' version: link:../tsconfig + '@faker-js/faker': + specifier: ^9.9.0 + version: 9.9.0 '@types/bcryptjs': - specifier: ^2.4.0 + specifier: ^2.4.6 version: 2.4.6 '@types/express': specifier: ^5.0.0 @@ -661,12 +796,34 @@ importers: '@types/express-serve-static-core': specifier: ^5.0.0 version: 5.1.0 + '@types/multer': + specifier: ^2.0.0 + version: 2.0.0 + '@types/node': + specifier: ^22.7.5 + version: 22.19.3 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/passport-local': + specifier: ^1.0.38 + version: 1.0.38 + '@types/pdf-parse': + specifier: ^1.1.5 + version: 1.1.5 + '@types/pg': + specifier: ^8.15.6 + version: 8.16.0 + '@types/turndown': + specifier: ^5.0.5 + version: 5.0.6 + tsup: + specifier: ^8.5.1 + version: 8.5.1(@swc/core@1.15.13)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.2) typescript: - specifier: ^5.6.3 + specifier: ^5.3.3 version: 5.9.3 - packages/biome-config: {} - packages/cv-renderer: dependencies: handlebars: @@ -689,14 +846,14 @@ importers: specifier: ^3.800.0 version: 3.1013.0 '@nestjs/common': - specifier: ^10.4.7 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@nestjs/config': - specifier: ^3.2.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + specifier: ^4.0.4 + version: 4.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) '@nestjs/core': - specifier: ^10.4.7 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + specifier: ^11.1.18 + version: 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) devDependencies: '@biomejs/biome': specifier: ^2.2.6 @@ -741,106 +898,76 @@ importers: '@cv/tsconfig': specifier: '*' version: link:../tsconfig - '@types/jest': - specifier: ^29.5.0 - version: 29.5.14 '@types/node': specifier: ^22.7.5 version: 22.19.3 '@types/pdf-parse': specifier: ^1.1.5 version: 1.1.5 - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - ts-jest: - specifier: ^29.1.1 - version: 29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3) typescript: specifier: ^5.3.3 version: 5.9.3 + vitest: + specifier: ^4.0.0 + version: 4.0.16(@types/node@22.19.3)(@vitest/ui@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) - packages/routing: - dependencies: - react: - specifier: ^19.2.3 - version: 19.2.3 - react-dom: - specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) - react-router-dom: - specifier: ^7.9.4 - version: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - devDependencies: - '@cv/tsconfig': - specifier: workspace:^ - version: link:../tsconfig - '@types/react': - specifier: ^19.0.6 - version: 19.2.7 - '@types/react-dom': - specifier: ^19.0.3 - version: 19.2.3(@types/react@19.2.7) - typescript: - specifier: ^5.5.3 - version: 5.9.3 - - packages/system: + packages/handlers: dependencies: + '@cv/core': + specifier: workspace:* + version: link:../core + '@cv/file-storage': + specifier: workspace:* + version: link:../file-storage '@nestjs/common': - specifier: ^10.4.7 - version: 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/config': - specifier: ^3.2.0 - version: 3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) - '@nestjs/core': - specifier: ^10.4.7 - version: 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/event-emitter': - specifier: ^3.0.1 - version: 3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - '@nestjs/graphql': - specifier: ^12.2.2 - version: 12.2.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2) - '@prisma/adapter-pg': - specifier: ^7.1.0 - version: 7.2.0 - '@prisma/client': - specifier: ^7.1.0 - version: 7.2.0(prisma@7.2.0(@types/react@19.2.7)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3) - dataloader: - specifier: ^2.2.3 - version: 2.2.3 - graphql: - specifier: ^16.12.0 - version: 16.12.0 - handlebars: - specifier: ^4.7.8 - version: 4.7.8 - pg: - specifier: ^8.16.3 - version: 8.16.3 - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - resend: - specifier: ^6.5.2 - version: 6.6.0 + specifier: ^11.1.18 + version: 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@riotbyte-com/project-q-core': + specifier: 1.0.19-rc.3 + version: 1.0.19-rc.3 + '@riotbyte-com/project-q-nestjs': + specifier: 1.0.19-rc.3 + version: 1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3)) + playwright: + specifier: ^1.52.0 + version: 1.58.2 + playwright-core: + specifier: ^1.52.0 + version: 1.58.2 devDependencies: - '@biomejs/biome': - specifier: ^2.2.6 - version: 2.3.11 - '@cv/biome-config': - specifier: '*' - version: link:../biome-config '@cv/tsconfig': specifier: '*' version: link:../tsconfig - '@types/pg': - specifier: ^8.15.0 - version: 8.16.0 + '@types/node': + specifier: ^22.7.5 + version: 22.19.3 typescript: - specifier: ^5.6.3 + specifier: ^5.3.3 + version: 5.9.3 + + packages/routing: + dependencies: + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + react-router-dom: + specifier: ^7.9.4 + version: 7.11.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + devDependencies: + '@cv/tsconfig': + specifier: workspace:^ + version: link:../tsconfig + '@types/react': + specifier: ^19.0.6 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.0.3 + version: 19.2.3(@types/react@19.2.7) + typescript: + specifier: ^5.5.3 version: 5.9.3 packages/tsconfig: {} @@ -911,46 +1038,34 @@ packages: peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/protobufjs@1.2.6': - resolution: {integrity: sha512-Wqo1oSHNUj/jxmsVp4iR3I480p6qdqHikn38lKrFhfzcDJ7lwd7Ck7cHRl4JE81tWNArl77xhnG/OkZhxKBYOw==} - hasBin: true - '@apollo/protobufjs@1.2.7': resolution: {integrity: sha512-Lahx5zntHPZia35myYDBRuF58tlwPskwHc5CWBZC/4bMKB6siTBWwtMrkqXcsNwQiFSzSx5hKdRPUmemrEp3Gg==} hasBin: true - '@apollo/server-gateway-interface@1.1.1': - resolution: {integrity: sha512-pGwCl/po6+rxRmDMFgozKQo2pbsSwE91TpsDBAOgf74CRDPXHHtM88wbwjab0wMMZh95QfR45GGyDIdhY24bkQ==} - deprecated: '@apollo/server-gateway-interface v1 is part of Apollo Server v4, which is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v2 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details.' + '@apollo/server-gateway-interface@2.0.0': + resolution: {integrity: sha512-3HEMD6fSantG2My3jWkb9dvfkF9vJ4BDLRjMgsnD790VINtuPaEp+h3Hg9HOHiWkML6QsOhnaRqZ+gvhp3y8Nw==} peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/server-plugin-landing-page-graphql-playground@4.0.0': - resolution: {integrity: sha512-PBDtKI/chJ+hHeoJUUH9Kuqu58txQl00vUGuxqiC9XcReulIg7RjsyD0G1u3drX4V709bxkL5S0nTeXfRHD0qA==} + '@apollo/server-plugin-landing-page-graphql-playground@4.0.1': + resolution: {integrity: sha512-tWhQzD7DtiTO/wfbGvasryz7eJSuEh9XJHgRTMZI7+Wu/omylG5gH6K6ksg1Vccg8/Xuglfi2f1M5Nm/IlBBGw==} engines: {node: '>=14.0'} deprecated: The use of GraphQL Playground in Apollo Server was supported in previous versions, but this is no longer the case as of December 31, 2022. This package exists for v4 migration purposes only. We do not intend to resolve security issues or other bugs with this package if they arise, so please migrate away from this to [Apollo Server's default Explorer](https://www.apollographql.com/docs/apollo-server/api/plugin/landing-pages) as soon as possible. peerDependencies: '@apollo/server': ^4.0.0 - '@apollo/server@4.12.2': - resolution: {integrity: sha512-jKRlf+sBMMdKYrjMoiWKne42Eb6paBfDOr08KJnUaeaiyWFj+/040FjVPQI7YGLfdwnYIsl1NUUqS2UdgezJDg==} - engines: {node: '>=14.16.0'} - deprecated: Apollo Server v4 is deprecated and will transition to end-of-life on January 26, 2026. As long as you are already using a non-EOL version of Node.js, upgrading to v5 should take only a few minutes. See https://www.apollographql.com/docs/apollo-server/previous-versions for details. + '@apollo/server@5.5.0': + resolution: {integrity: sha512-vWtodBOK/SZwBTJzItECOmLfL8E8pn/IdvP7pnxN5g2tny9iW4+9sxdajE798wV1H2+PYp/rRcl/soSHIBKMPw==} + engines: {node: '>=20'} peerDependencies: - graphql: ^16.6.0 + graphql: ^16.11.0 '@apollo/usage-reporting-protobuf@4.1.1': resolution: {integrity: sha512-u40dIUePHaSKVshcedO7Wp+mPiZsaU6xjv9J+VyxpoU/zL6Jle+9zWeG98tr/+SZ0nZ4OXhrbb8SNr0rAPpIDA==} - '@apollo/utils.createhash@2.0.2': - resolution: {integrity: sha512-UkS3xqnVFLZ3JFpEmU/2cM2iKJotQXMoSTgxXsfQgXLC5gR1WaepoXagmYnPSA7Q/2cmnyTYK5OgAgoC4RULPg==} - engines: {node: '>=14'} - - '@apollo/utils.dropunuseddefinitions@1.1.0': - resolution: {integrity: sha512-jU1XjMr6ec9pPoL+BFWzEPW7VHHulVdGKMkPAMiCigpVIT11VmCbnij0bWob8uS3ODJ65tZLYKAh/55vLw2rbg==} - engines: {node: '>=12.13.0'} - peerDependencies: - graphql: 14.x || 15.x || 16.x + '@apollo/utils.createhash@3.0.1': + resolution: {integrity: sha512-CKrlySj4eQYftBE5MJ8IzKwIibQnftDT7yGfsJy5KSEEnLlPASX0UTpbKqkjlVEwPPd4mEwI7WOM7XNxEuO05A==} + engines: {node: '>=16'} '@apollo/utils.dropunuseddefinitions@2.0.1': resolution: {integrity: sha512-EsPIBqsSt2BwDsv8Wu76LK5R1KtsVkNoO4b0M5aK0hx+dGg9xJXuqlr7Fo34Dl+y83jmzn+UvEW+t1/GP2melA==} @@ -958,33 +1073,21 @@ packages: peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/utils.fetcher@2.0.1': - resolution: {integrity: sha512-jvvon885hEyWXd4H6zpWeN3tl88QcWnHp5gWF5OPF34uhvoR+DFqcNxs9vrRaBBSY3qda3Qe0bdud7tz2zGx1A==} - engines: {node: '>=14'} - - '@apollo/utils.isnodelike@2.0.1': - resolution: {integrity: sha512-w41XyepR+jBEuVpoRM715N2ZD0xMD413UiJx8w5xnAZD2ZkSJnMJBoIzauK83kJpSgNuR6ywbV29jG9NmxjK0Q==} - engines: {node: '>=14'} - - '@apollo/utils.keyvaluecache@1.0.2': - resolution: {integrity: sha512-p7PVdLPMnPzmXSQVEsy27cYEjVON+SH/Wb7COyW3rQN8+wJgT1nv9jZouYtztWW8ZgTkii5T6tC9qfoDREd4mg==} - - '@apollo/utils.keyvaluecache@2.1.1': - resolution: {integrity: sha512-qVo5PvUUMD8oB9oYvq4ViCjYAMWnZ5zZwEjNF37L2m1u528x5mueMlU+Cr1UinupCgdB78g+egA1G98rbJ03Vw==} - engines: {node: '>=14'} + '@apollo/utils.fetcher@3.1.0': + resolution: {integrity: sha512-Z3QAyrsQkvrdTuHAFwWDNd+0l50guwoQUoaDQssLOjkmnmVuvXlJykqlEJolio+4rFwBnWdoY1ByFdKaQEcm7A==} + engines: {node: '>=16'} - '@apollo/utils.logger@1.0.1': - resolution: {integrity: sha512-XdlzoY7fYNK4OIcvMD2G94RoFZbzTQaNP0jozmqqMudmaGo2I/2Jx71xlDJ801mWA/mbYRihyaw6KJii7k5RVA==} + '@apollo/utils.isnodelike@3.0.0': + resolution: {integrity: sha512-xrjyjfkzunZ0DeF6xkHaK5IKR8F1FBq6qV+uZ+h9worIF/2YSzA0uoBxGv6tbTeo9QoIQnRW4PVFzGix5E7n/g==} + engines: {node: '>=16'} - '@apollo/utils.logger@2.0.1': - resolution: {integrity: sha512-YuplwLHaHf1oviidB7MxnCXAdHp3IqYV8n0momZ3JfLniae92eYqMIx+j5qJFX6WKJPs6q7bczmV4lXIsTu5Pg==} - engines: {node: '>=14'} + '@apollo/utils.keyvaluecache@4.0.0': + resolution: {integrity: sha512-mKw1myRUkQsGPNB+9bglAuhviodJ2L2MRYLTafCMw5BIo7nbvCPNCkLnIHjZ1NOzH7SnMAr5c9LmXiqsgYqLZw==} + engines: {node: '>=20'} - '@apollo/utils.printwithreducedwhitespace@1.1.0': - resolution: {integrity: sha512-GfFSkAv3n1toDZ4V6u2d7L4xMwLA+lv+6hqXicMN9KELSJ9yy9RzuEXaX73c/Ry+GzRsBy/fdSUGayGqdHfT2Q==} - engines: {node: '>=12.13.0'} - peerDependencies: - graphql: 14.x || 15.x || 16.x + '@apollo/utils.logger@3.0.0': + resolution: {integrity: sha512-M8V8JOTH0F2qEi+ktPfw4RL7MvUycDfKp7aEap2eWXfL5SqWHN6jTLbj5f5fj1cceHpyaUSOZlvlaaryaxZAmg==} + engines: {node: '>=16'} '@apollo/utils.printwithreducedwhitespace@2.0.1': resolution: {integrity: sha512-9M4LUXV/fQBh8vZWlLvb/HyyhjJ77/I5ZKu+NBWV/BmYGyRmoEP9EVAy7LCVoY3t8BDcyCAGfxJaLFCSuQkPUg==} @@ -992,63 +1095,33 @@ packages: peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/utils.removealiases@1.0.0': - resolution: {integrity: sha512-6cM8sEOJW2LaGjL/0vHV0GtRaSekrPQR4DiywaApQlL9EdROASZU5PsQibe2MWeZCOhNrPRuHh4wDMwPsWTn8A==} - engines: {node: '>=12.13.0'} - peerDependencies: - graphql: 14.x || 15.x || 16.x - '@apollo/utils.removealiases@2.0.1': resolution: {integrity: sha512-0joRc2HBO4u594Op1nev+mUF6yRnxoUH64xw8x3bX7n8QBDYdeYgY4tF0vJReTy+zdn2xv6fMsquATSgC722FA==} engines: {node: '>=14'} peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/utils.sortast@1.1.0': - resolution: {integrity: sha512-VPlTsmUnOwzPK5yGZENN069y6uUHgeiSlpEhRnLFYwYNoJHsuJq2vXVwIaSmts015WTPa2fpz1inkLYByeuRQA==} - engines: {node: '>=12.13.0'} - peerDependencies: - graphql: 14.x || 15.x || 16.x - '@apollo/utils.sortast@2.0.1': resolution: {integrity: sha512-eciIavsWpJ09za1pn37wpsCGrQNXUhM0TktnZmHwO+Zy9O4fu/WdB4+5BvVhFiZYOXvfjzJUcc+hsIV8RUOtMw==} engines: {node: '>=14'} peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/utils.stripsensitiveliterals@1.2.0': - resolution: {integrity: sha512-E41rDUzkz/cdikM5147d8nfCFVKovXxKBcjvLEQ7bjZm/cg9zEcXvS6vFY8ugTubI3fn6zoqo0CyU8zT+BGP9w==} - engines: {node: '>=12.13.0'} - peerDependencies: - graphql: 14.x || 15.x || 16.x - '@apollo/utils.stripsensitiveliterals@2.0.1': resolution: {integrity: sha512-QJs7HtzXS/JIPMKWimFnUMK7VjkGQTzqD9bKD1h3iuPAqLsxd0mUNVbkYOPTsDhUKgcvUOfOqOJWYohAKMvcSA==} engines: {node: '>=14'} peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/utils.usagereporting@1.0.1': - resolution: {integrity: sha512-6dk+0hZlnDbahDBB2mP/PZ5ybrtCJdLMbeNJD+TJpKyZmSY6bA3SjI8Cr2EM9QA+AdziywuWg+SgbWUF3/zQqQ==} - engines: {node: '>=12.13.0'} - peerDependencies: - graphql: 14.x || 15.x || 16.x - '@apollo/utils.usagereporting@2.1.0': resolution: {integrity: sha512-LPSlBrn+S17oBy5eWkrRSGb98sWmnEzo3DPTZgp8IQc8sJe0prDgDuppGq4NeQlpoqEHz0hQeYHAOA0Z3aQsxQ==} engines: {node: '>=14'} peerDependencies: graphql: 14.x || 15.x || 16.x - '@apollo/utils.withrequired@2.0.1': - resolution: {integrity: sha512-YBDiuAX9i1lLc6GeTy1m7DGLFn/gMnvXqlalOIMjM7DeOgIacEjjfwPqb0M1CQ2v11HhR15d1NmxJoRCfrNqcA==} - engines: {node: '>=14'} - - '@apollographql/apollo-tools@0.5.4': - resolution: {integrity: sha512-shM3q7rUbNyXVVRkQJQseXv6bnYM3BUma/eZhwXR4xsuM+bqWnJKvW7SAfRjP7LuSCocrexa5AXhjjawNHrIlw==} - engines: {node: '>=8', npm: '>=6'} - peerDependencies: - graphql: ^14.2.1 || ^15.0.0 || ^16.0.0 + '@apollo/utils.withrequired@3.0.0': + resolution: {integrity: sha512-aaxeavfJ+RHboh7c2ofO5HHtQobGX4AgUujXP4CXpREHp9fQ9jPi6K9T1jrAKe7HIipoP0OJ1gd6JamSkFIpvA==} + engines: {node: '>=16'} '@apollographql/graphql-playground-html@1.6.29': resolution: {integrity: sha512-xCcXpoz52rI4ksJSdOCxeOCn2DLocxwHf9dVT/Q90Pte1LX+LY+91SFtJF3KXVHH8kEin+g1KKCQPKBjZJfWNA==} @@ -1065,6 +1138,13 @@ packages: peerDependencies: graphql: '*' + '@as-integrations/express5@1.1.2': + resolution: {integrity: sha512-BxfwtcWNf2CELDkuPQxi5Zl3WqY/dQVJYafeCBOGoFQjv5M0fjhxmAFZ9vKx/5YKKNeok4UY6PkFbHzmQrdxIA==} + engines: {node: '>=20'} + peerDependencies: + '@apollo/server': ^4.0.0 || ^5.0.0 + express: ^5.0.0 + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -1325,27 +1405,11 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-class-properties@7.12.13': resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-flow@7.27.1': resolution: {integrity: sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==} engines: {node: '>=6.9.0'} @@ -1358,76 +1422,17 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.27.1': - resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.27.1': resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-object-rest-spread@7.8.3': resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.27.1': - resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-arrow-functions@7.27.1': resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} engines: {node: '>=6.9.0'} @@ -1576,9 +1581,6 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@biomejs/biome@2.3.11': resolution: {integrity: sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==} engines: {node: '>=14.21.3'} @@ -2250,30 +2252,15 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/merge@8.3.1': - resolution: {integrity: sha512-BMm99mqdNZbEYeTPK3it9r9S6rsZsQKtlqJsSBknAclXq2pGEfOxjcIZi+kBSkHZKPKCRrYDd5vY0+rUmIHVLg==} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/merge@8.4.2': - resolution: {integrity: sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/merge@9.0.11': - resolution: {integrity: sha512-AJL0XTozn31HoZN8tULzEkbDXyETA5vAFu4Q65kxJDu027p+auaNFYj/y51HP4BhMR4wykoteWyO7/VxKfdpiw==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/merge@9.1.6': resolution: {integrity: sha512-bTnP+4oom4nDjmkS3Ykbe+ljAp/RIiWP3R35COMmuucS24iQxGLa9Hn8VMkLIoaoPxgz6xk+dbC43jtkNsFoBw==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/mock@8.7.20': - resolution: {integrity: sha512-ljcHSJWjC/ZyzpXd5cfNhPI7YljRVvabKHPzKjEs5ElxWu2cdlLGvyNYepApXDsM/OJG/2xuhGM+9GWu5gEAPQ==} + '@graphql-tools/merge@9.1.7': + resolution: {integrity: sha512-Y5E1vTbTabvcXbkakdFUt4zUIzB1fyaEnVmIWN0l0GMed2gdD01TpZWLUm4RNAxpturvolrb24oGLQrBbPLSoQ==} + engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 @@ -2299,25 +2286,15 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/schema@10.0.10': - resolution: {integrity: sha512-TSdDvwgk1Fq3URDuZBMCPXlWLpRpxwaQ+0KqvycVwoHozYnBRZ2Ql9HVgDKnebkGQKmIk2enSeku+ERKxxSG0g==} - engines: {node: '>=16.0.0'} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/schema@10.0.30': resolution: {integrity: sha512-yPXU17uM/LR90t92yYQqn9mAJNOVZJc0nQtYeZyZeQZeQjwIGlTubvvoDL0fFVk+wZzs4YQOgds2NwSA4npodA==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/schema@8.5.1': - resolution: {integrity: sha512-0Esilsh0P/qYcB5DKQpiKeQs/jevzIadNTaT0jeWklPMwNbT7yMX4EqZany7mbeRRlSRwMzNzL5olyFdffHBZg==} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@graphql-tools/schema@9.0.19': - resolution: {integrity: sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w==} + '@graphql-tools/schema@10.0.31': + resolution: {integrity: sha512-ZewRgWhXef6weZ0WiP7/MV47HXiuFbFpiDUVLQl6mgXsWSsGELKFxQsyUCBos60Qqy1JEFAIu3Ns6GGYjGkqkQ==} + engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 @@ -2339,17 +2316,12 @@ packages: peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/utils@10.6.1': - resolution: {integrity: sha512-XHl0/DWkMf/8Dmw1F3RRoMPt6ZwU4J707YWcbPjS+49WZNoTVz6f+prQ4GuwZT8RqTPtrRawnGU93AV73ZLTfQ==} + '@graphql-tools/utils@11.0.0': + resolution: {integrity: sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/utils@8.9.0': - resolution: {integrity: sha512-pjJIWH0XOVnYGXCqej8g/u/tsfV4LvLlj0eATKQu5zwnxd/TiTHq7Cg313qUPTFFHZ3PP5wJ15chYVtLDwaymg==} - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - '@graphql-tools/utils@9.2.1': resolution: {integrity: sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==} peerDependencies: @@ -2528,83 +2500,10 @@ packages: '@isaacs/string-locale-compare@1.1.0': resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==} - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} - - '@istanbuljs/schema@0.1.3': - resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} - engines: {node: '>=8'} - - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - '@jest/schemas@29.6.3': resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@josephg/resolvable@1.0.1': - resolution: {integrity: sha512-CtzORUwWTTOTqfVtHaKRJ0I1kNQd1bpn3sUh8I3nJDVY+5/M/Oe1DnEWzPQvqq/xPIIkzzzIP7mfCoAjFRvDhg==} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -2646,6 +2545,9 @@ packages: peerDependencies: rollup: '>=2' + '@mixmark-io/domino@2.2.0': + resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} + '@mrleebo/prisma-ast@0.12.1': resolution: {integrity: sha512-JwqeCQ1U3fvccttHZq7Tk0m/TMC6WcFAQZdukypW3AzlJYKYTGNVd1ANU2GuhKnv4UQuOFj3oAl0LLG/gxFN1w==} engines: {node: '>=16'} @@ -2653,30 +2555,33 @@ packages: '@napi-rs/wasm-runtime@0.2.4': resolution: {integrity: sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ==} - '@nestjs/apollo@12.2.2': - resolution: {integrity: sha512-gsDqSfsmTSvF0k3XaRESRgM3uE/YFO+59txCsq7T1EadDOVOuoF3zVQiFmi6D50Rlnqohqs63qjjf46mgiiXgQ==} + '@nestjs/apollo@13.2.5': + resolution: {integrity: sha512-UaBdfpbXMBzSMZOG4eGkTMHyRxaPUx0sw46dOFWAyF/WDmS7xJHd6KR+iRHSa4atQzzMXvtd9ZVYacHxNfuLKA==} peerDependencies: '@apollo/gateway': ^2.0.0 - '@apollo/server': ^4.3.2 + '@apollo/server': ^5.0.0 '@apollo/subgraph': ^2.0.0 - '@as-integrations/fastify': ^1.3.0 || ^2.0.0 - '@nestjs/common': ^9.3.8 || ^10.0.0 - '@nestjs/core': ^9.3.8 || ^10.0.0 - '@nestjs/graphql': ^12.0.0 - graphql: ^16.6.0 + '@as-integrations/express5': '*' + '@as-integrations/fastify': ^2.1.1 || ^3.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + '@nestjs/graphql': ^13.0.0 + graphql: ^16.10.0 peerDependenciesMeta: '@apollo/gateway': optional: true '@apollo/subgraph': optional: true + '@as-integrations/express5': + optional: true '@as-integrations/fastify': optional: true - '@nestjs/common@10.4.20': - resolution: {integrity: sha512-hxJxZF7jcKGuUzM9EYbuES80Z/36piJbiqmPy86mk8qOn5gglFebBTvcx7PWVbRNSb4gngASYnefBj/Y2HAzpQ==} + '@nestjs/common@11.1.11': + resolution: {integrity: sha512-R/+A8XFqLgN8zNs2twhrOaE7dJbRQhdPX3g46am4RT/x8xGLqDphrXkUIno4cGUZHxbczChBAaAPTdPv73wDZA==} peerDependencies: - class-transformer: '*' - class-validator: '*' + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' reflect-metadata: ^0.1.12 || ^0.2.0 rxjs: ^7.1.0 peerDependenciesMeta: @@ -2685,8 +2590,8 @@ packages: class-validator: optional: true - '@nestjs/common@11.1.11': - resolution: {integrity: sha512-R/+A8XFqLgN8zNs2twhrOaE7dJbRQhdPX3g46am4RT/x8xGLqDphrXkUIno4cGUZHxbczChBAaAPTdPv73wDZA==} + '@nestjs/common@11.1.18': + resolution: {integrity: sha512-0sLq8Z+TIjLnz1Tqp0C/x9BpLbqpt1qEu0VcH4/fkE0y3F5JxhfK1AdKQ/SPbKhKgwqVDoY4gS8GQr2G6ujaWg==} peerDependencies: class-transformer: '>=0.4.1' class-validator: '>=0.13.2' @@ -2698,25 +2603,26 @@ packages: class-validator: optional: true - '@nestjs/config@3.3.0': - resolution: {integrity: sha512-pdGTp8m9d0ZCrjTpjkUbZx6gyf2IKf+7zlkrPNMsJzYZ4bFRRTpXrnj+556/5uiI6AfL5mMrJc2u7dB6bvM+VA==} + '@nestjs/config@4.0.2': + resolution: {integrity: sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==} peerDependencies: - '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/config@4.0.2': - resolution: {integrity: sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==} + '@nestjs/config@4.0.4': + resolution: {integrity: sha512-CJPjNitr0bAufSEnRe2N+JbnVmMmDoo6hvKCPzXgZoGwJSmp/dZPk9f/RMbuD/+Q1ZJPjwsRpq0vxna++Knwow==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 rxjs: ^7.1.0 - '@nestjs/core@10.4.20': - resolution: {integrity: sha512-kRdtyKA3+Tu70N3RQ4JgmO1E3LzAMs/eppj7SfjabC7TgqNWoS4RLhWl4BqmsNVmjj6D5jgfPVtHtgYkU3AfpQ==} + '@nestjs/core@11.1.18': + resolution: {integrity: sha512-wR3DtGyk/LUAiPtbXDuWJJwVkWElKBY0sqnTzf9d4uM3+X18FRZhK7WFc47czsIGOdWuRsMeLYV+1Z9dO4zDEQ==} + engines: {node: '>= 20'} peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/microservices': ^10.0.0 - '@nestjs/platform-express': ^10.0.0 - '@nestjs/websockets': ^10.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 reflect-metadata: ^0.1.12 || ^0.2.0 rxjs: ^7.1.0 peerDependenciesMeta: @@ -2733,17 +2639,17 @@ packages: '@nestjs/common': ^10.0.0 || ^11.0.0 '@nestjs/core': ^10.0.0 || ^11.0.0 - '@nestjs/graphql@12.2.2': - resolution: {integrity: sha512-lUDy/1uqbRA1kBKpXcmY0aHhcPbfeG52Wg5+9Jzd1d57dwSjCAmuO+mWy5jz9ugopVCZeK0S/kdAMvA+r9fNdA==} + '@nestjs/graphql@13.2.5': + resolution: {integrity: sha512-CtM24scQtyP5Xgewidd0VIH4H0LS5i1zESrlM2In7QF7snv+ANdCWz6bgVf1qCZwOTl1jKLY4mYnLPgimPGTyQ==} peerDependencies: - '@apollo/subgraph': ^2.0.0 - '@nestjs/common': ^9.3.8 || ^10.0.0 - '@nestjs/core': ^9.3.8 || ^10.0.0 + '@apollo/subgraph': ^2.9.3 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 class-transformer: '*' class-validator: '*' - graphql: ^16.6.0 + graphql: ^16.11.0 reflect-metadata: ^0.1.13 || ^0.2.0 - ts-morph: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^24.0.0 + ts-morph: ^20.0.0 || ^21.0.0 || ^24.0.0 || ^25.0.0 || ^26.0.0 || ^27.0.0 peerDependenciesMeta: '@apollo/subgraph': optional: true @@ -2754,30 +2660,17 @@ packages: ts-morph: optional: true - '@nestjs/jwt@10.2.0': - resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==} - peerDependencies: - '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 - - '@nestjs/mapped-types@2.0.6': - resolution: {integrity: sha512-84ze+CPfp1OWdpRi1/lOu59hOhTz38eVzJvRKrg9ykRFwDz+XleKfMsG0gUqNZYFa6v53XYzeD+xItt8uDW7NQ==} + '@nestjs/jwt@11.0.2': + resolution: {integrity: sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==} peerDependencies: - '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 - class-transformer: ^0.4.0 || ^0.5.0 - class-validator: ^0.13.0 || ^0.14.0 - reflect-metadata: ^0.1.12 || ^0.2.0 - peerDependenciesMeta: - class-transformer: - optional: true - class-validator: - optional: true + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 - '@nestjs/mapped-types@2.1.0': - resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} + '@nestjs/mapped-types@2.1.1': + resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==} peerDependencies: '@nestjs/common': ^10.0.0 || ^11.0.0 class-transformer: ^0.4.0 || ^0.5.0 - class-validator: ^0.13.0 || ^0.14.0 + class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0 reflect-metadata: ^0.1.12 || ^0.2.0 peerDependenciesMeta: class-transformer: @@ -2785,25 +2678,25 @@ packages: class-validator: optional: true - '@nestjs/passport@10.0.3': - resolution: {integrity: sha512-znJ9Y4S8ZDVY+j4doWAJ8EuuVO7SkQN3yOBmzxbGaXbvcSwFDAdGJ+OMCg52NdzIO4tQoN4pYKx8W6M0ArfFRQ==} + '@nestjs/passport@11.0.5': + resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==} peerDependencies: - '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 - passport: ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 - '@nestjs/platform-express@10.4.20': - resolution: {integrity: sha512-rh97mX3rimyf4xLMLHuTOBKe6UD8LOJ14VlJ1F/PTd6C6ZK9Ak6EHuJvdaGcSFQhd3ZMBh3I6CuujKGW9pNdIg==} + '@nestjs/platform-express@11.1.18': + resolution: {integrity: sha512-s6GdHMTa3qx0fJewR74Xa30ysPHfBEqxIwZ7BGSTLoAEQ1vTP24urNl+b6+s49NFLEIOyeNho5fN/9/I17QlOw==} peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/core': ^10.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 - '@nestjs/testing@10.4.20': - resolution: {integrity: sha512-nMkRDukDKskdPruM6EsgMq7yJua+CPZM6I6FrLP8yXw8BiVSPv9Nm0CtcGGwt3kgZF9hfxKjGqLjsvVBsv6Vfw==} + '@nestjs/testing@11.1.18': + resolution: {integrity: sha512-frzwNlpBgtAzI3hp/qo57DZoRO4RMTH1wST3QUYEhRTHyfPkLpzkWz3jV/mhApXjD0yT56Ptlzn6zuYPLh87Lw==} peerDependencies: - '@nestjs/common': ^10.0.0 - '@nestjs/core': ^10.0.0 - '@nestjs/microservices': ^10.0.0 - '@nestjs/platform-express': ^10.0.0 + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 peerDependenciesMeta: '@nestjs/microservices': optional: true @@ -2884,9 +2777,9 @@ packages: resolution: {integrity: sha512-y7efHHwghQfk28G2z3tlZ67pLG0XdfYbcVG26r7YIXALRsrVQcTq4/tdenSmdOrEsNahIYA/eh8aEVROWGFUDg==} engines: {node: ^16.14.0 || >=18.0.0} - '@nuxtjs/opencollective@0.3.2': - resolution: {integrity: sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} + '@nuxt/opencollective@0.4.1': + resolution: {integrity: sha512-GXD3wy50qYbxCJ652bDrDzgMr3NFEkIS374+IgFQKkCvk9yiYcLvX2XDYr7UyQxf4wK0e+yqDYRubZ0DtOxnmQ==} + engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'} hasBin: true '@nx/devkit@20.8.3': @@ -3394,12 +3287,6 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@smithy/abort-controller@4.2.12': resolution: {integrity: sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==} engines: {node: '>=18.0.0'} @@ -3837,10 +3724,6 @@ packages: peerDependencies: graphql: ^16.0.0 - '@tokenizer/inflate@0.2.7': - resolution: {integrity: sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==} - engines: {node: '>=18'} - '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -3871,9 +3754,6 @@ packages: '@tybys/wasm-util@0.9.0': resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} - '@types/accepts@1.3.7': - resolution: {integrity: sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -3889,9 +3769,6 @@ packages: '@types/bcryptjs@2.4.6': resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} - '@types/body-parser@1.19.2': - resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} - '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -3906,9 +3783,6 @@ packages: peerDependencies: '@types/express': '*' - '@types/cors@2.8.12': - resolution: {integrity: sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -3921,27 +3795,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@4.17.31': - resolution: {integrity: sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==} - - '@types/express-serve-static-core@4.19.7': - resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} - '@types/express-serve-static-core@5.1.0': resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} - '@types/express@4.17.14': - resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==} - - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - '@types/express@5.0.6': resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - '@types/handlebars@4.1.0': resolution: {integrity: sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA==} deprecated: This is a stub types definition. handlebars provides its own type definitions, so you do not need this installed. @@ -3958,24 +3817,9 @@ packages: '@types/inquirer@8.2.12': resolution: {integrity: sha512-YxURZF2ZsSjU5TAe06tW0M3sL4UI9AMPA6dd8I72uOtppzNafcY38xkYgCZ/vsVOAyNdzHmvtTpLWilOrbP0dQ==} - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - - '@types/jest@29.5.14': - resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} - '@types/jsonwebtoken@9.0.10': resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - '@types/jsonwebtoken@9.0.5': - resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} - '@types/long@4.0.2': resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} @@ -3985,9 +3829,6 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - '@types/minimatch@3.0.5': resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} @@ -4000,12 +3841,6 @@ packages: '@types/multer@2.0.0': resolution: {integrity: sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==} - '@types/node-fetch@2.6.13': - resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} - - '@types/node@10.17.60': - resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} - '@types/node@22.19.3': resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} @@ -4050,24 +3885,18 @@ packages: '@types/react@19.2.7': resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==} - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - '@types/send@1.2.1': resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} - '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - '@types/through@0.0.33': resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + '@types/turndown@5.0.6': + resolution: {integrity: sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -4080,12 +3909,6 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@17.0.35': - resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -4168,8 +3991,8 @@ packages: resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} acorn-jsx@5.3.2: @@ -4236,56 +4059,6 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - apollo-datasource@3.3.2: - resolution: {integrity: sha512-L5TiS8E2Hn/Yz7SSnWIVbZw0ZfEIXZCa5VUiVxD9P53JvSrf4aStvsFDlGWPvpIdCR+aly2CfoB79B9/JjKFqg==} - engines: {node: '>=12.0'} - deprecated: The `apollo-datasource` package is part of Apollo Server v2 and v3, which are now end-of-life (as of October 22nd 2023 and October 22nd 2024, respectively). See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details. - - apollo-reporting-protobuf@3.4.0: - resolution: {integrity: sha512-h0u3EbC/9RpihWOmcSsvTW2O6RXVaD/mPEjfrPkxRPTEPWqncsgOoRJw+wih4OqfH3PvTJvoEIf4LwKrUaqWog==} - deprecated: The `apollo-reporting-protobuf` package is part of Apollo Server v2 and v3, which are now end-of-life (as of October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/usage-reporting-protobuf` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details. - - apollo-server-core@3.13.0: - resolution: {integrity: sha512-v/g6DR6KuHn9DYSdtQijz8dLOkP78I5JSVJzPkARhDbhpH74QNwrQ2PP2URAPPEDJ2EeZNQDX8PvbYkAKqg+kg==} - engines: {node: '>=12.0'} - deprecated: The `apollo-server-core` package is part of Apollo Server v2 and v3, which are now end-of-life (as of October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details. - peerDependencies: - graphql: ^15.3.0 || ^16.0.0 - - apollo-server-env@4.2.1: - resolution: {integrity: sha512-vm/7c7ld+zFMxibzqZ7SSa5tBENc4B0uye9LTfjJwGoQFY5xsUPH5FpO5j0bMUDZ8YYNbrF9SNtzc5Cngcr90g==} - engines: {node: '>=12.0'} - deprecated: The `apollo-server-env` package is part of Apollo Server v2 and v3, which are now end-of-life (as of October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/utils.fetcher` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details. - - apollo-server-errors@3.3.1: - resolution: {integrity: sha512-xnZJ5QWs6FixHICXHxUfm+ZWqqxrNuPlQ+kj5m6RtEgIpekOPssH/SD9gf2B4HuWV0QozorrygwZnux8POvyPA==} - engines: {node: '>=12.0'} - deprecated: The `apollo-server-errors` package is part of Apollo Server v2 and v3, which are now end-of-life (as of October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details. - peerDependencies: - graphql: ^15.3.0 || ^16.0.0 - - apollo-server-express@3.13.0: - resolution: {integrity: sha512-iSxICNbDUyebOuM8EKb3xOrpIwOQgKxGbR2diSr4HP3IW8T3njKFOoMce50vr+moOCe1ev8BnLcw9SNbuUtf7g==} - engines: {node: '>=12.0'} - deprecated: The `apollo-server-express` package is part of Apollo Server v2 and v3, which are now end-of-life (as of October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details. - peerDependencies: - express: ^4.17.1 - graphql: ^15.3.0 || ^16.0.0 - - apollo-server-plugin-base@3.7.2: - resolution: {integrity: sha512-wE8dwGDvBOGehSsPTRZ8P/33Jan6/PmL0y0aN/1Z5a5GcbFhDaaJCjK5cav6npbbGL2DPKK0r6MPXi3k3N45aw==} - engines: {node: '>=12.0'} - deprecated: The `apollo-server-plugin-base` package is part of Apollo Server v2 and v3, which are now end-of-life (as of October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details. - peerDependencies: - graphql: ^15.3.0 || ^16.0.0 - - apollo-server-types@3.8.0: - resolution: {integrity: sha512-ZI/8rTE4ww8BHktsVpb91Sdq7Cb71rdSkXELSwdSR0eXu600/sY+1UXhTWdiJvk+Eq5ljqoHLwLbY2+Clq2b9A==} - engines: {node: '>=12.0'} - deprecated: The `apollo-server-types` package is part of Apollo Server v2 and v3, which are now end-of-life (as of October 22nd 2023 and October 22nd 2024, respectively). This package's functionality is now found in the `@apollo/server` package. See https://www.apollographql.com/docs/apollo-server/previous-versions/ for more details. - peerDependencies: - graphql: ^15.3.0 || ^16.0.0 - append-field@1.0.0: resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} @@ -4305,9 +4078,6 @@ packages: resolution: {integrity: sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==} engines: {node: '>=8'} - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - array-ify@1.0.0: resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==} @@ -4369,39 +4139,14 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: resolution: {integrity: sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==} - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} - peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 - babel-preset-fbjs@3.4.0: resolution: {integrity: sha512-9ywCsCvo1ojrw0b+XYk7aFvTH6D9064t0RIL1rtMf3nsa02Xw41MS7sZw216Im35xj/UY0PDBQsa1brUDDF1Ow==} peerDependencies: '@babel/core': ^7.0.0 - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - backo2@1.0.2: resolution: {integrity: sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==} @@ -4441,13 +4186,9 @@ packages: bmp-js@0.1.0: resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} - body-parser@1.20.3: - resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -4467,10 +4208,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -4544,10 +4281,6 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - caniuse-lite@1.0.30001762: resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} @@ -4579,10 +4312,6 @@ packages: change-case@4.1.2: resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} @@ -4608,10 +4337,6 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} - chokidar@4.0.1: - resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} - engines: {node: '>= 14.16.0'} - chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} @@ -4634,9 +4359,6 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - class-transformer@0.5.1: resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} @@ -4704,16 +4426,9 @@ packages: resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - collapse-white-space@2.1.0: resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} - collect-v8-coverage@1.0.3: - resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -4776,9 +4491,6 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} - consola@2.15.3: - resolution: {integrity: sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==} - consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} @@ -4789,9 +4501,9 @@ packages: constant-case@3.0.4: resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} content-type@1.0.5: resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} @@ -4838,9 +4550,9 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} - cookie@0.7.1: - resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} - engines: {node: '>= 0.6'} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} cookie@0.7.2: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} @@ -4856,8 +4568,8 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} cosmiconfig@8.3.6: @@ -4878,11 +4590,6 @@ packages: typescript: optional: true - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -4926,14 +4633,6 @@ packages: resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} engines: {node: '>=18'} - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -5022,10 +4721,6 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - detect-indent@5.0.0: resolution: {integrity: sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==} engines: {node: '>=4'} @@ -5043,10 +4738,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -5075,10 +4766,6 @@ packages: resolution: {integrity: sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==} engines: {node: '>=8'} - dotenv-expand@10.0.0: - resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} - engines: {node: '>=12'} - dotenv-expand@11.0.7: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} engines: {node: '>=12'} @@ -5087,8 +4774,8 @@ packages: resolution: {integrity: sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==} engines: {node: '>=12'} - dotenv@16.4.5: - resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} engines: {node: '>=12'} dotenv@16.4.7: @@ -5099,6 +4786,10 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} + dotenv@17.4.1: + resolution: {integrity: sha512-k8DaKGP6r1G30Lx8V4+pCsLzKr8vLmV2paqEj1Y55GdAgJuIqpRp5FfajGF8KtwMxCz9qJc6wUIJnm053d/WCw==} + engines: {node: '>=12'} + dset@3.1.4: resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} engines: {node: '>=4'} @@ -5130,10 +4821,6 @@ packages: electron-to-chromium@1.5.267: resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==} - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} - emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -5147,10 +4834,6 @@ packages: resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} engines: {node: '>=14'} - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -5237,10 +4920,6 @@ packages: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} @@ -5294,10 +4973,6 @@ packages: resolution: {integrity: sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==} engines: {node: '>=10'} - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -5306,16 +4981,12 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - express@4.21.2: - resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} - engines: {node: '>= 0.10.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} @@ -5331,17 +5002,10 @@ packages: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} @@ -5391,14 +5055,14 @@ packages: resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} engines: {node: '>= 12'} - file-type@20.4.1: - resolution: {integrity: sha512-hw9gNZXUfZ02Jo0uafWLaFVPter5/k2rfcrjFJJHX/77xtSDOfJuEFb6oKlFV86FLP1SuyHMW1PSk0U9M5tKkQ==} - engines: {node: '>=18'} - file-type@21.2.0: resolution: {integrity: sha512-vCYBgFOrJQLoTzDyAXAL/RFfKnXXpUYt4+tipVy26nJJhT7ftgGETf2tAQF59EEL61i3MrorV/PG6tf7LJK7eg==} engines: {node: '>=20'} + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -5406,9 +5070,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - finalhandler@1.3.1: - resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} - engines: {node: '>= 0.8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} find-up@2.1.0: resolution: {integrity: sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==} @@ -5463,9 +5127,9 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} front-matter@4.0.2: resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} @@ -5520,10 +5184,6 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - get-pkg-repo@4.2.1: resolution: {integrity: sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA==} engines: {node: '>=6.9.0'} @@ -5584,15 +5244,17 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@9.3.5: resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} engines: {node: '>=16 || 14 >=14.17'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} @@ -5640,12 +5302,6 @@ packages: peerDependencies: graphql: '>=0.8.0' - graphql-ws@5.16.0: - resolution: {integrity: sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==} - engines: {node: '>=10'} - peerDependencies: - graphql: '>=0.11 <=16' - graphql-ws@6.0.6: resolution: {integrity: sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==} engines: {node: '>=20'} @@ -5665,6 +5321,22 @@ packages: ws: optional: true + graphql-ws@6.0.8: + resolution: {integrity: sha512-m3EOaNsUBXwAnkBWbzPfe0Nq8pXUfxsWnolC54sru3FzHvhTZL0Ouf/BoQsaGAXqM+YPerXOJ47BUnmgmoupCw==} + engines: {node: '>=20'} + peerDependencies: + '@fastify/websocket': ^10 || ^11 + crossws: ~0.3 + graphql: ^15.10.1 || ^16 + ws: ^8 + peerDependenciesMeta: + '@fastify/websocket': + optional: true + crossws: + optional: true + ws: + optional: true + graphql@16.12.0: resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} @@ -5741,19 +5413,12 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} - html-escaper@2.0.2: - resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} - html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -5922,10 +5587,6 @@ packages: resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} engines: {node: '>=18'} - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -5963,6 +5624,9 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} @@ -6034,30 +5698,6 @@ packages: peerDependencies: ws: '*' - istanbul-lib-coverage@3.2.2: - resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - - istanbul-lib-report@3.0.1: - resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} - engines: {node: '>=10'} - - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} - - istanbul-reports@3.2.0: - resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} - engines: {node: '>=8'} - iterall@1.3.0: resolution: {integrity: sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==} @@ -6073,135 +5713,14 @@ packages: engines: {node: '>=10'} hasBin: true - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - jest-diff@29.7.0: resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-get-type@29.6.3: resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -6261,10 +5780,6 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} - jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} - engines: {node: '>=12', npm: '>=6'} - jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} @@ -6278,15 +5793,9 @@ packages: just-diff@6.0.2: resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==} - jwa@1.4.2: - resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} - jwa@2.0.1: resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - jws@3.2.3: - resolution: {integrity: sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==} - jws@4.0.1: resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} @@ -6294,19 +5803,11 @@ packages: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} engines: {node: '>=0.10.0'} - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - lerna@8.2.4: resolution: {integrity: sha512-0gaVWDIVT7fLfprfwpYcQajb7dBJv3EGavjG7zvJ+TmGx3/wovl5GklnSwM2/WeE0Z2wrIz7ndWhBcDUHVjOcQ==} engines: {node: '>=18.0.0'} hasBin: true - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - libnpmaccess@8.0.6: resolution: {integrity: sha512-uM8DHDEfYG6G5gVivVl+yQd4pH3uRclHC59lzIbSvy7b5FEwR+mU49Zq1jEyRtRFv7+M99mUW9S0wL/4laT4lw==} engines: {node: ^16.14.0 || >=18.0.0} @@ -6459,12 +5960,8 @@ packages: lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - - lodash.omit@4.5.0: - resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} - deprecated: This package is deprecated. Use destructuring assignment syntax instead. + lodash.omit@4.18.0: + resolution: {integrity: sha512-hZXIupXdHtocTnvIJ2aCd2vxKYtxex6gbiGuPvgBRnFQO9yu3AtmDAbVuCXcSsQx3INo/1g71OktlFFA/ES8Xg==} lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} @@ -6475,6 +5972,9 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} @@ -6515,6 +6015,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.3.3: + resolution: {integrity: sha512-JvNw9Y81y33E+BEYPr0U7omo+U9AySnsMsEiXgwT6yqd31VQWTLNQqmT4ou5eqPFUrTfIDFta2wKhB1hyohtAQ==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -6522,14 +6026,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lru-cache@7.13.1: - resolution: {integrity: sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==} - engines: {node: '>=12'} - - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - lru.min@1.1.3: resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} @@ -6552,9 +6048,6 @@ packages: resolution: {integrity: sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==} engines: {node: ^16.14.0 || >=18.0.0} - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - mammoth@1.11.0: resolution: {integrity: sha512-BcEqqY/BOwIcI1iR5tqyVlqc3KIaMRa4egSoK83YAVrBf6+yqdAAbtUcFDCWX8Zef8/fgNZ6rl4VUv+vVX8ddQ==} engines: {node: '>=12.0.0'} @@ -6635,12 +6128,17 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + meow@8.1.2: resolution: {integrity: sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==} engines: {node: '>=10'} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -6775,14 +6273,17 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} mime@2.6.0: resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} @@ -6877,10 +6378,6 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -6901,14 +6398,11 @@ packages: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multer@2.0.2: - resolution: {integrity: sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==} + multer@2.1.1: + resolution: {integrity: sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==} engines: {node: '>= 10.16.0'} multimatch@5.0.0: @@ -6945,17 +6439,14 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - negotiator@0.6.4: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} @@ -6977,6 +6468,10 @@ packages: '@nestjs/swagger': optional: true + neverthrow@8.2.0: + resolution: {integrity: sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==} + engines: {node: '>=18'} + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -6984,9 +6479,6 @@ packages: resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} engines: {node: '>=10'} - node-abort-controller@3.1.1: - resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} - node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} @@ -7333,11 +6825,8 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} - path-to-regexp@0.1.12: - resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} - - path-to-regexp@3.3.0: - resolution: {integrity: sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} path-type@3.0.0: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} @@ -7575,10 +7064,6 @@ packages: promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - promzard@1.0.2: resolution: {integrity: sha512-2FPputGL+mP3jJ3UZg/Dl9YOkovB7DX0oOr+ck5QbZ5MtORtds8k/BZdn+02peDLI8/YWbmzx34k5fA+fHvCVQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -7611,10 +7096,6 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - qs@6.13.0: - resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} - engines: {node: '>=0.6'} - qs@6.14.1: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} @@ -7633,13 +7114,9 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@2.5.2: - resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} - engines: {node: '>= 0.8'} - - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -7894,6 +7371,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-async@2.4.1: resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} engines: {node: '>=0.12.0'} @@ -7929,9 +7410,9 @@ packages: engines: {node: '>=10'} hasBin: true - send@0.19.0: - resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} - engines: {node: '>= 0.8.0'} + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} @@ -7939,9 +7420,9 @@ packages: seq-queue@0.0.5: resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} - serve-static@1.16.2: - resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} - engines: {node: '>= 0.8.0'} + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} @@ -8027,9 +7508,6 @@ packages: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -8061,9 +7539,6 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -8111,17 +7586,9 @@ packages: resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -8139,10 +7606,6 @@ packages: string-env-interpolation@1.0.1: resolution: {integrity: sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==} - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -8196,10 +7659,6 @@ packages: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - strnum@2.2.1: resolution: {integrity: sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==} @@ -8242,10 +7701,6 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -8295,10 +7750,6 @@ packages: tesseract.js@5.1.1: resolution: {integrity: sha512-lzVl/Ar3P3zhpUT31NjqeCo1f+D5+YfpZ5J62eo2S14QNVOmHBTtbchHm/YAbOOOzCegFnKf4B3Qih9LuldcYQ==} - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - text-extensions@1.9.0: resolution: {integrity: sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==} engines: {node: '>=0.10'} @@ -8353,9 +7804,6 @@ packages: resolution: {integrity: sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==} engines: {node: '>=14.14'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - to-buffer@1.2.2: resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} engines: {node: '>= 0.4'} @@ -8394,42 +7842,15 @@ packages: trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - trim-newlines@3.0.1: - resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} - engines: {node: '>=8'} - - trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - ts-jest@29.4.6: - resolution: {integrity: sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 || ^30.0.0 - '@jest/types': ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 - esbuild: '*' - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <6' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - jest-util: - optional: true + trim-newlines@3.0.1: + resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} + engines: {node: '>=8'} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} ts-log@2.2.7: resolution: {integrity: sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==} @@ -8487,9 +7908,9 @@ packages: tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} + turndown@7.2.4: + resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==} + engines: {node: '>=18', npm: '>=9'} type-fest@0.18.1: resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} @@ -8519,6 +7940,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typed-array-buffer@1.0.3: resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} engines: {node: '>= 0.4'} @@ -8648,17 +8073,13 @@ packages: resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} hasBin: true - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} - valibot@1.2.0: resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} peerDependencies: @@ -8678,14 +8099,6 @@ packages: resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} engines: {node: '>= 0.10'} - value-or-promise@1.0.11: - resolution: {integrity: sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==} - engines: {node: '>=12'} - - value-or-promise@1.0.12: - resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==} - engines: {node: '>=12'} - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -8804,9 +8217,6 @@ packages: walk-up-path@3.0.1: resolution: {integrity: sha512-9YlCL/ynK3CTlrSRrDxZvUauLzAswPCrsaCgilqFevUYpeEW0/3ScEjaa3kbW/T0ghhkEr7mv+fpjqn1Y1YuTA==} - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - wasm-feature-detect@1.8.0: resolution: {integrity: sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==} @@ -8820,10 +8230,6 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - whatwg-mimetype@3.0.0: - resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} - engines: {node: '>=12'} - whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} @@ -8881,10 +8287,6 @@ packages: write-file-atomic@2.4.3: resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - write-file-atomic@5.0.1: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -8909,8 +8311,8 @@ packages: utf-8-validate: optional: true - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -8921,8 +8323,8 @@ packages: utf-8-validate: optional: true - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -9020,22 +8422,6 @@ snapshots: dependencies: graphql: 16.12.0 - '@apollo/protobufjs@1.2.6': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 - '@types/long': 4.0.2 - '@types/node': 10.17.60 - long: 4.0.0 - '@apollo/protobufjs@1.2.7': dependencies: '@protobufjs/aspromise': 1.1.2 @@ -9051,129 +8437,87 @@ snapshots: '@types/long': 4.0.2 long: 4.0.0 - '@apollo/server-gateway-interface@1.1.1(graphql@16.12.0)': + '@apollo/server-gateway-interface@2.0.0(graphql@16.12.0)': dependencies: '@apollo/usage-reporting-protobuf': 4.1.1 - '@apollo/utils.fetcher': 2.0.1 - '@apollo/utils.keyvaluecache': 2.1.1 - '@apollo/utils.logger': 2.0.1 + '@apollo/utils.fetcher': 3.1.0 + '@apollo/utils.keyvaluecache': 4.0.0 + '@apollo/utils.logger': 3.0.0 graphql: 16.12.0 - '@apollo/server-plugin-landing-page-graphql-playground@4.0.0(@apollo/server@4.12.2(encoding@0.1.13)(graphql@16.12.0))': + '@apollo/server-plugin-landing-page-graphql-playground@4.0.1(@apollo/server@5.5.0(graphql@16.12.0))': dependencies: - '@apollo/server': 4.12.2(encoding@0.1.13)(graphql@16.12.0) + '@apollo/server': 5.5.0(graphql@16.12.0) '@apollographql/graphql-playground-html': 1.6.29 - '@apollo/server@4.12.2(encoding@0.1.13)(graphql@16.12.0)': + '@apollo/server@5.5.0(graphql@16.12.0)': dependencies: '@apollo/cache-control-types': 1.0.3(graphql@16.12.0) - '@apollo/server-gateway-interface': 1.1.1(graphql@16.12.0) + '@apollo/server-gateway-interface': 2.0.0(graphql@16.12.0) '@apollo/usage-reporting-protobuf': 4.1.1 - '@apollo/utils.createhash': 2.0.2 - '@apollo/utils.fetcher': 2.0.1 - '@apollo/utils.isnodelike': 2.0.1 - '@apollo/utils.keyvaluecache': 2.1.1 - '@apollo/utils.logger': 2.0.1 + '@apollo/utils.createhash': 3.0.1 + '@apollo/utils.fetcher': 3.1.0 + '@apollo/utils.isnodelike': 3.0.0 + '@apollo/utils.keyvaluecache': 4.0.0 + '@apollo/utils.logger': 3.0.0 '@apollo/utils.usagereporting': 2.1.0(graphql@16.12.0) - '@apollo/utils.withrequired': 2.0.1 - '@graphql-tools/schema': 9.0.19(graphql@16.12.0) - '@types/express': 4.17.25 - '@types/express-serve-static-core': 4.19.7 - '@types/node-fetch': 2.6.13 + '@apollo/utils.withrequired': 3.0.0 + '@graphql-tools/schema': 10.0.31(graphql@16.12.0) async-retry: 1.3.3 - cors: 2.8.5 - express: 4.21.2 + body-parser: 2.2.2 + content-type: 1.0.5 + cors: 2.8.6 + finalhandler: 2.1.1 graphql: 16.12.0 loglevel: 1.9.2 - lru-cache: 7.18.3 - negotiator: 0.6.4 - node-abort-controller: 3.1.1 - node-fetch: 2.7.0(encoding@0.1.13) - uuid: 9.0.1 - whatwg-mimetype: 3.0.0 + lru-cache: 11.3.3 + negotiator: 1.0.0 + uuid: 11.1.0 + whatwg-mimetype: 4.0.0 transitivePeerDependencies: - - encoding - supports-color '@apollo/usage-reporting-protobuf@4.1.1': dependencies: '@apollo/protobufjs': 1.2.7 - '@apollo/utils.createhash@2.0.2': + '@apollo/utils.createhash@3.0.1': dependencies: - '@apollo/utils.isnodelike': 2.0.1 + '@apollo/utils.isnodelike': 3.0.0 sha.js: 2.4.12 - '@apollo/utils.dropunuseddefinitions@1.1.0(graphql@16.12.0)': - dependencies: - graphql: 16.12.0 - '@apollo/utils.dropunuseddefinitions@2.0.1(graphql@16.12.0)': dependencies: graphql: 16.12.0 - '@apollo/utils.fetcher@2.0.1': {} - - '@apollo/utils.isnodelike@2.0.1': {} + '@apollo/utils.fetcher@3.1.0': {} - '@apollo/utils.keyvaluecache@1.0.2': - dependencies: - '@apollo/utils.logger': 1.0.1 - lru-cache: 7.13.1 + '@apollo/utils.isnodelike@3.0.0': {} - '@apollo/utils.keyvaluecache@2.1.1': + '@apollo/utils.keyvaluecache@4.0.0': dependencies: - '@apollo/utils.logger': 2.0.1 - lru-cache: 7.18.3 - - '@apollo/utils.logger@1.0.1': {} + '@apollo/utils.logger': 3.0.0 + lru-cache: 11.3.3 - '@apollo/utils.logger@2.0.1': {} - - '@apollo/utils.printwithreducedwhitespace@1.1.0(graphql@16.12.0)': - dependencies: - graphql: 16.12.0 + '@apollo/utils.logger@3.0.0': {} '@apollo/utils.printwithreducedwhitespace@2.0.1(graphql@16.12.0)': dependencies: graphql: 16.12.0 - '@apollo/utils.removealiases@1.0.0(graphql@16.12.0)': - dependencies: - graphql: 16.12.0 - '@apollo/utils.removealiases@2.0.1(graphql@16.12.0)': dependencies: graphql: 16.12.0 - '@apollo/utils.sortast@1.1.0(graphql@16.12.0)': - dependencies: - graphql: 16.12.0 - lodash.sortby: 4.7.0 - '@apollo/utils.sortast@2.0.1(graphql@16.12.0)': dependencies: graphql: 16.12.0 lodash.sortby: 4.7.0 - '@apollo/utils.stripsensitiveliterals@1.2.0(graphql@16.12.0)': - dependencies: - graphql: 16.12.0 - '@apollo/utils.stripsensitiveliterals@2.0.1(graphql@16.12.0)': dependencies: graphql: 16.12.0 - '@apollo/utils.usagereporting@1.0.1(graphql@16.12.0)': - dependencies: - '@apollo/usage-reporting-protobuf': 4.1.1 - '@apollo/utils.dropunuseddefinitions': 1.1.0(graphql@16.12.0) - '@apollo/utils.printwithreducedwhitespace': 1.1.0(graphql@16.12.0) - '@apollo/utils.removealiases': 1.0.0(graphql@16.12.0) - '@apollo/utils.sortast': 1.1.0(graphql@16.12.0) - '@apollo/utils.stripsensitiveliterals': 1.2.0(graphql@16.12.0) - graphql: 16.12.0 - '@apollo/utils.usagereporting@2.1.0(graphql@16.12.0)': dependencies: '@apollo/usage-reporting-protobuf': 4.1.1 @@ -9184,11 +8528,7 @@ snapshots: '@apollo/utils.stripsensitiveliterals': 2.0.1(graphql@16.12.0) graphql: 16.12.0 - '@apollo/utils.withrequired@2.0.1': {} - - '@apollographql/apollo-tools@0.5.4(graphql@16.12.0)': - dependencies: - graphql: 16.12.0 + '@apollo/utils.withrequired@3.0.0': {} '@apollographql/graphql-playground-html@1.6.29': dependencies: @@ -9234,6 +8574,11 @@ snapshots: transitivePeerDependencies: - encoding + '@as-integrations/express5@1.1.2(@apollo/server@5.5.0(graphql@16.12.0))(express@5.2.1)': + dependencies: + '@apollo/server': 5.5.0(graphql@16.12.0) + express: 5.2.1 + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -9818,26 +9163,11 @@ snapshots: '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.5) '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.5) - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-flow@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -9848,71 +9178,16 @@ snapshots: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - - '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': - dependencies: - '@babel/core': 7.28.5 - '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -10082,8 +9357,6 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@bcoe/v8-coverage@0.2.3': {} - '@biomejs/biome@2.3.11': optionalDependencies: '@biomejs/cli-darwin-arm64': 2.3.11 @@ -10340,10 +9613,10 @@ snapshots: commander: 11.1.0 prettier: 3.8.1 - '@golevelup/nestjs-discovery@4.0.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)': + '@golevelup/nestjs-discovery@4.0.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) lodash: 4.17.21 '@graphql-codegen/add@6.0.0(graphql@16.12.0)': @@ -10797,35 +10070,15 @@ snapshots: p-limit: 3.1.0 tslib: 2.8.1 - '@graphql-tools/merge@8.3.1(graphql@16.12.0)': - dependencies: - '@graphql-tools/utils': 8.9.0(graphql@16.12.0) - graphql: 16.12.0 - tslib: 2.8.1 - - '@graphql-tools/merge@8.4.2(graphql@16.12.0)': - dependencies: - '@graphql-tools/utils': 9.2.1(graphql@16.12.0) - graphql: 16.12.0 - tslib: 2.8.1 - - '@graphql-tools/merge@9.0.11(graphql@16.12.0)': - dependencies: - '@graphql-tools/utils': 10.6.1(graphql@16.12.0) - graphql: 16.12.0 - tslib: 2.8.1 - '@graphql-tools/merge@9.1.6(graphql@16.12.0)': dependencies: '@graphql-tools/utils': 10.11.0(graphql@16.12.0) graphql: 16.12.0 tslib: 2.8.1 - '@graphql-tools/mock@8.7.20(graphql@16.12.0)': + '@graphql-tools/merge@9.1.7(graphql@16.12.0)': dependencies: - '@graphql-tools/schema': 9.0.19(graphql@16.12.0) - '@graphql-tools/utils': 9.2.1(graphql@16.12.0) - fast-json-stable-stringify: 2.1.0 + '@graphql-tools/utils': 11.0.0(graphql@16.12.0) graphql: 16.12.0 tslib: 2.8.1 @@ -10858,14 +10111,6 @@ snapshots: transitivePeerDependencies: - encoding - '@graphql-tools/schema@10.0.10(graphql@16.12.0)': - dependencies: - '@graphql-tools/merge': 9.0.11(graphql@16.12.0) - '@graphql-tools/utils': 10.6.1(graphql@16.12.0) - graphql: 16.12.0 - tslib: 2.8.1 - value-or-promise: 1.0.12 - '@graphql-tools/schema@10.0.30(graphql@16.12.0)': dependencies: '@graphql-tools/merge': 9.1.6(graphql@16.12.0) @@ -10873,21 +10118,12 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 - '@graphql-tools/schema@8.5.1(graphql@16.12.0)': - dependencies: - '@graphql-tools/merge': 8.3.1(graphql@16.12.0) - '@graphql-tools/utils': 8.9.0(graphql@16.12.0) - graphql: 16.12.0 - tslib: 2.8.1 - value-or-promise: 1.0.11 - - '@graphql-tools/schema@9.0.19(graphql@16.12.0)': + '@graphql-tools/schema@10.0.31(graphql@16.12.0)': dependencies: - '@graphql-tools/merge': 8.4.2(graphql@16.12.0) - '@graphql-tools/utils': 9.2.1(graphql@16.12.0) + '@graphql-tools/merge': 9.1.7(graphql@16.12.0) + '@graphql-tools/utils': 11.0.0(graphql@16.12.0) graphql: 16.12.0 tslib: 2.8.1 - value-or-promise: 1.0.12 '@graphql-tools/url-loader@8.0.33(@types/node@22.19.3)(graphql@16.12.0)': dependencies: @@ -10943,16 +10179,11 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 - '@graphql-tools/utils@10.6.1(graphql@16.12.0)': + '@graphql-tools/utils@11.0.0(graphql@16.12.0)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) + '@whatwg-node/promise-helpers': 1.3.2 cross-inspect: 1.0.1 - dset: 3.1.4 - graphql: 16.12.0 - tslib: 2.8.1 - - '@graphql-tools/utils@8.9.0(graphql@16.12.0)': - dependencies: graphql: 16.12.0 tslib: 2.8.1 @@ -11070,240 +10301,70 @@ snapshots: dependencies: '@inquirer/ansi': 1.0.2 '@inquirer/core': 10.3.2(@types/node@22.19.3) - '@inquirer/type': 3.0.10(@types/node@22.19.3) - optionalDependencies: - '@types/node': 22.19.3 - - '@inquirer/prompts@7.10.1(@types/node@22.19.3)': - dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@22.19.3) - '@inquirer/confirm': 5.1.21(@types/node@22.19.3) - '@inquirer/editor': 4.2.23(@types/node@22.19.3) - '@inquirer/expand': 4.0.23(@types/node@22.19.3) - '@inquirer/input': 4.3.1(@types/node@22.19.3) - '@inquirer/number': 3.0.23(@types/node@22.19.3) - '@inquirer/password': 4.0.23(@types/node@22.19.3) - '@inquirer/rawlist': 4.1.11(@types/node@22.19.3) - '@inquirer/search': 3.2.2(@types/node@22.19.3) - '@inquirer/select': 4.4.2(@types/node@22.19.3) - optionalDependencies: - '@types/node': 22.19.3 - - '@inquirer/rawlist@4.1.11(@types/node@22.19.3)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.3) - '@inquirer/type': 3.0.10(@types/node@22.19.3) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 22.19.3 - - '@inquirer/search@3.2.2(@types/node@22.19.3)': - dependencies: - '@inquirer/core': 10.3.2(@types/node@22.19.3) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.3) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 22.19.3 - - '@inquirer/select@4.4.2(@types/node@22.19.3)': - dependencies: - '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@22.19.3) - '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@22.19.3) - yoctocolors-cjs: 2.1.3 - optionalDependencies: - '@types/node': 22.19.3 - - '@inquirer/type@3.0.10(@types/node@22.19.3)': - optionalDependencies: - '@types/node': 22.19.3 - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@isaacs/string-locale-compare@1.1.0': {} - - '@istanbuljs/load-nyc-config@1.1.0': - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.2 - resolve-from: 5.0.0 - - '@istanbuljs/schema@0.1.3': {} - - '@jest/console@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - jest-mock: 29.7.0 - - '@jest/expect-utils@29.7.0': - dependencies: - jest-get-type: 29.6.3 - - '@jest/expect@29.7.0': - dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/fake-timers@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 + '@inquirer/type': 3.0.10(@types/node@22.19.3) + optionalDependencies: '@types/node': 22.19.3 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 - '@jest/globals@29.7.0': + '@inquirer/prompts@7.10.1(@types/node@22.19.3)': dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 - transitivePeerDependencies: - - supports-color + '@inquirer/checkbox': 4.3.2(@types/node@22.19.3) + '@inquirer/confirm': 5.1.21(@types/node@22.19.3) + '@inquirer/editor': 4.2.23(@types/node@22.19.3) + '@inquirer/expand': 4.0.23(@types/node@22.19.3) + '@inquirer/input': 4.3.1(@types/node@22.19.3) + '@inquirer/number': 3.0.23(@types/node@22.19.3) + '@inquirer/password': 4.0.23(@types/node@22.19.3) + '@inquirer/rawlist': 4.1.11(@types/node@22.19.3) + '@inquirer/search': 3.2.2(@types/node@22.19.3) + '@inquirer/select': 4.4.2(@types/node@22.19.3) + optionalDependencies: + '@types/node': 22.19.3 - '@jest/reporters@29.7.0': + '@inquirer/rawlist@4.1.11(@types/node@22.19.3)': dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/type': 3.0.10(@types/node@22.19.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: '@types/node': 22.19.3 - chalk: 4.1.2 - collect-v8-coverage: 1.0.3 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.2.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color - '@jest/schemas@29.6.3': + '@inquirer/search@3.2.2(@types/node@22.19.3)': dependencies: - '@sinclair/typebox': 0.27.8 + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.3 - '@jest/source-map@29.6.3': + '@inquirer/select@4.4.2(@types/node@22.19.3)': dependencies: - '@jridgewell/trace-mapping': 0.3.31 - callsites: 3.1.0 - graceful-fs: 4.2.11 + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@22.19.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@22.19.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.19.3 - '@jest/test-result@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.3 + '@inquirer/type@3.0.10(@types/node@22.19.3)': + optionalDependencies: + '@types/node': 22.19.3 - '@jest/test-sequencer@29.7.0': + '@isaacs/cliui@8.0.2': dependencies: - '@jest/test-result': 29.7.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - slash: 3.0.0 + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 - '@jest/transform@29.7.0': - dependencies: - '@babel/core': 7.28.5 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color + '@isaacs/string-locale-compare@1.1.0': {} - '@jest/types@29.6.3': + '@jest/schemas@29.6.3': dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 22.19.3 - '@types/yargs': 17.0.35 - chalk: 4.1.2 - - '@josephg/resolvable@1.0.1': {} + '@sinclair/typebox': 0.27.8 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -11459,6 +10520,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@mixmark-io/domino@2.2.0': {} + '@mrleebo/prisma-ast@0.12.1': dependencies: chevrotain: 10.5.0 @@ -11470,22 +10533,25 @@ snapshots: '@emnapi/runtime': 1.8.1 '@tybys/wasm-util': 0.9.0 - '@nestjs/apollo@12.2.2(@apollo/server@4.12.2(encoding@0.1.13)(graphql@16.12.0))(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/graphql@12.2.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2))(graphql@16.12.0)': + '@nestjs/apollo@13.2.5(@apollo/server@5.5.0(graphql@16.12.0))(@as-integrations/express5@1.1.2(@apollo/server@5.5.0(graphql@16.12.0))(express@5.2.1))(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/graphql@13.2.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2))(graphql@16.12.0)': dependencies: - '@apollo/server': 4.12.2(encoding@0.1.13)(graphql@16.12.0) - '@apollo/server-plugin-landing-page-graphql-playground': 4.0.0(@apollo/server@4.12.2(encoding@0.1.13)(graphql@16.12.0)) - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/graphql': 12.2.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2) + '@apollo/server': 5.5.0(graphql@16.12.0) + '@apollo/server-plugin-landing-page-graphql-playground': 4.0.1(@apollo/server@5.5.0(graphql@16.12.0)) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/graphql': 13.2.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2) graphql: 16.12.0 iterall: 1.3.0 - lodash.omit: 4.5.0 + lodash.omit: 4.18.0 tslib: 2.8.1 + optionalDependencies: + '@as-integrations/express5': 1.1.2(@apollo/server@5.5.0(graphql@16.12.0))(express@5.2.1) - '@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 20.4.1 + file-type: 21.2.0 iterare: 1.2.1 + load-esm: 1.0.3 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 @@ -11496,9 +10562,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': dependencies: - file-type: 21.2.0 + file-type: 21.3.4 iterare: 1.2.1 load-esm: 1.0.3 reflect-metadata: 0.2.2 @@ -11511,14 +10577,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/config@3.3.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': - dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - dotenv: 16.4.5 - dotenv-expand: 10.0.0 - lodash: 4.17.21 - rxjs: 7.8.2 - '@nestjs/config@4.0.2(@nestjs/common@11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: '@nestjs/common': 11.1.11(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -11527,101 +10585,100 @@ snapshots: lodash: 4.17.21 rxjs: 7.8.2 - '@nestjs/core@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + '@nestjs/config@4.0.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nuxtjs/opencollective': 0.3.2(encoding@0.1.13) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 17.4.1 + dotenv-expand: 12.0.3 + lodash: 4.18.1 + rxjs: 7.8.2 + + '@nestjs/core@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nuxt/opencollective': 0.4.1 fast-safe-stringify: 2.1.1 iterare: 1.2.1 - path-to-regexp: 3.3.0 + path-to-regexp: 8.4.2 reflect-metadata: 0.2.2 rxjs: 7.8.2 tslib: 2.8.1 uid: 2.0.2 optionalDependencies: - '@nestjs/platform-express': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - transitivePeerDependencies: - - encoding + '@nestjs/platform-express': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) - '@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)': + '@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) eventemitter2: 6.4.9 - '@nestjs/graphql@12.2.2(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2)': + '@nestjs/graphql@13.2.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.14.3)(graphql@16.12.0)(reflect-metadata@0.2.2)': dependencies: - '@graphql-tools/merge': 9.0.11(graphql@16.12.0) - '@graphql-tools/schema': 10.0.10(graphql@16.12.0) - '@graphql-tools/utils': 10.6.1(graphql@16.12.0) - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/mapped-types': 2.0.6(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) - chokidar: 4.0.1 - fast-glob: 3.3.2 + '@graphql-tools/merge': 9.1.7(graphql@16.12.0) + '@graphql-tools/schema': 10.0.31(graphql@16.12.0) + '@graphql-tools/utils': 11.0.0(graphql@16.12.0) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2) + chokidar: 4.0.3 + fast-glob: 3.3.3 graphql: 16.12.0 graphql-tag: 2.12.6(graphql@16.12.0) - graphql-ws: 5.16.0(graphql@16.12.0) - lodash: 4.17.21 + graphql-ws: 6.0.8(graphql@16.12.0)(ws@8.20.0) + lodash: 4.18.1 normalize-path: 3.0.0 reflect-metadata: 0.2.2 subscriptions-transport-ws: 0.11.0(graphql@16.12.0) tslib: 2.8.1 - uuid: 11.0.3 - ws: 8.18.0 + ws: 8.20.0 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.3 transitivePeerDependencies: + - '@fastify/websocket' - bufferutil + - crossws - utf-8-validate - '@nestjs/jwt@10.2.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))': - dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@types/jsonwebtoken': 9.0.5 - jsonwebtoken: 9.0.2 - - '@nestjs/mapped-types@2.0.6(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/jwt@11.0.2(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - reflect-metadata: 0.2.2 - optionalDependencies: - class-transformer: 0.5.1 - class-validator: 0.14.3 + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.3 - '@nestjs/mapped-types@2.1.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) reflect-metadata: 0.2.2 optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.3 - '@nestjs/passport@10.0.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + '@nestjs/passport@11.0.5(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) passport: 0.7.0 - '@nestjs/platform-express@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)': + '@nestjs/platform-express@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - body-parser: 1.20.3 - cors: 2.8.5 - express: 4.21.2 - multer: 2.0.2 + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.1.1 + path-to-regexp: 8.4.2 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@nestjs/testing@10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@10.4.20)': + '@nestjs/testing@11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/platform-express@11.1.18)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) tslib: 2.8.1 optionalDependencies: - '@nestjs/platform-express': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/platform-express': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@noble/hashes@1.8.0': {} @@ -11767,13 +10824,9 @@ snapshots: - bluebird - supports-color - '@nuxtjs/opencollective@0.3.2(encoding@0.1.13)': + '@nuxt/opencollective@0.4.1': dependencies: - chalk: 4.1.2 - consola: 2.15.3 - node-fetch: 2.6.7(encoding@0.1.13) - transitivePeerDependencies: - - encoding + consola: 3.4.2 '@nx/devkit@20.8.3(nx@20.8.3(@swc/core@1.15.13))': dependencies: @@ -12065,33 +11118,33 @@ snapshots: '@repeaterjs/repeater@3.0.6': {} - '@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)': + '@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@riotbyte-com/project-q-core@1.0.19-rc.3': dependencies: - uuid: 11.0.3 + uuid: 11.1.0 zod: 4.3.6 - '@riotbyte-com/project-q-nestjs@1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3))': + '@riotbyte-com/project-q-nestjs@1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3))': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/event-emitter': 3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - '@riotbyte-com/nest-service-locator': 0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/event-emitter': 3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@riotbyte-com/nest-service-locator': 0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) '@riotbyte-com/project-q-core': 1.0.19-rc.3 eventemitter2: 6.4.9 - nest-commander: 3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3) - uuid: 11.0.3 + nest-commander: 3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3) + uuid: 11.1.0 zod: 4.3.6 - '@riotbyte-com/project-q-prisma@1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@riotbyte-com/project-q-core@1.0.19-rc.3)(@riotbyte-com/project-q-nestjs@1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3)))': + '@riotbyte-com/project-q-prisma@1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@riotbyte-com/project-q-core@1.0.19-rc.3)(@riotbyte-com/project-q-nestjs@1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3)))': dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@riotbyte-com/project-q-core': 1.0.19-rc.3 - '@riotbyte-com/project-q-nestjs': 1.0.19-rc.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/event-emitter@3.0.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3)) + '@riotbyte-com/project-q-nestjs': 1.0.19-rc.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@nestjs/event-emitter@3.0.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(@riotbyte-com/nest-service-locator@0.3.0-rc.4(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18))(eventemitter2@6.4.9)(nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3)) uuid: 11.0.3 zod: 4.3.6 @@ -12205,14 +11258,6 @@ snapshots: '@sinclair/typebox@0.27.8': {} - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@10.3.0': - dependencies: - '@sinonjs/commons': 3.0.1 - '@smithy/abort-controller@4.2.12': dependencies: '@smithy/types': 4.13.1 @@ -12736,14 +11781,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@tokenizer/inflate@0.2.7': - dependencies: - debug: 4.4.3(supports-color@5.5.0) - fflate: 0.8.2 - token-types: 6.1.2 - transitivePeerDependencies: - - supports-color - '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3(supports-color@5.5.0) @@ -12772,10 +11809,6 @@ snapshots: dependencies: tslib: 2.8.1 - '@types/accepts@1.3.7': - dependencies: - '@types/node': 22.19.3 - '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -12799,11 +11832,6 @@ snapshots: '@types/bcryptjs@2.4.6': {} - '@types/body-parser@1.19.2': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 22.19.3 - '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -12822,8 +11850,6 @@ snapshots: dependencies: '@types/express': 5.0.6 - '@types/cors@2.8.12': {} - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -12836,19 +11862,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@4.17.31': - dependencies: - '@types/node': 22.19.3 - '@types/qs': 6.14.0 - '@types/range-parser': 1.2.7 - - '@types/express-serve-static-core@4.19.7': - dependencies: - '@types/node': 22.19.3 - '@types/qs': 6.14.0 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - '@types/express-serve-static-core@5.1.0': dependencies: '@types/node': 22.19.3 @@ -12856,30 +11869,12 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 1.2.1 - '@types/express@4.17.14': - dependencies: - '@types/body-parser': 1.19.2 - '@types/express-serve-static-core': 4.19.7 - '@types/qs': 6.14.0 - '@types/serve-static': 2.2.0 - - '@types/express@4.17.25': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.7 - '@types/qs': 6.14.0 - '@types/serve-static': 1.15.10 - '@types/express@5.0.6': dependencies: '@types/body-parser': 1.19.6 '@types/express-serve-static-core': 5.1.0 '@types/serve-static': 2.2.0 - '@types/graceful-fs@4.1.9': - dependencies: - '@types/node': 22.19.3 - '@types/handlebars@4.1.0': dependencies: handlebars: 4.7.8 @@ -12897,30 +11892,11 @@ snapshots: '@types/through': 0.0.33 rxjs: 7.8.2 - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 - - '@types/jest@29.5.14': - dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - '@types/jsonwebtoken@9.0.10': dependencies: '@types/ms': 2.1.0 '@types/node': 22.19.3 - '@types/jsonwebtoken@9.0.5': - dependencies: - '@types/node': 22.19.3 - '@types/long@4.0.2': {} '@types/mdast@4.0.4': @@ -12929,8 +11905,6 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/mime@1.3.5': {} - '@types/minimatch@3.0.5': {} '@types/minimist@1.2.5': {} @@ -12941,13 +11915,6 @@ snapshots: dependencies: '@types/express': 5.0.6 - '@types/node-fetch@2.6.13': - dependencies: - '@types/node': 22.19.3 - form-data: 4.0.5 - - '@types/node@10.17.60': {} - '@types/node@22.19.3': dependencies: undici-types: 6.21.0 @@ -13007,32 +11974,21 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/send@0.17.6': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 22.19.3 - '@types/send@1.2.1': dependencies: '@types/node': 22.19.3 - '@types/serve-static@1.15.10': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 22.19.3 - '@types/send': 0.17.6 - '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 '@types/node': 22.19.3 - '@types/stack-utils@2.0.3': {} - '@types/through@0.0.33': dependencies: '@types/node': 22.19.3 + '@types/turndown@5.0.6': {} + '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} @@ -13043,12 +11999,6 @@ snapshots: dependencies: '@types/node': 22.19.3 - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@17.0.35': - dependencies: - '@types/yargs-parser': 21.0.3 - '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@22.19.3)(lightningcss@1.30.2))': @@ -13166,10 +12116,10 @@ snapshots: abbrev@2.0.0: {} - accepts@1.3.8: + accepts@2.0.0: dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 + mime-types: 3.0.2 + negotiator: 1.0.0 acorn-jsx@5.3.2(acorn@8.15.0): dependencies: @@ -13219,92 +12169,6 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - apollo-datasource@3.3.2(encoding@0.1.13): - dependencies: - '@apollo/utils.keyvaluecache': 1.0.2 - apollo-server-env: 4.2.1(encoding@0.1.13) - transitivePeerDependencies: - - encoding - - apollo-reporting-protobuf@3.4.0: - dependencies: - '@apollo/protobufjs': 1.2.6 - - apollo-server-core@3.13.0(encoding@0.1.13)(graphql@16.12.0): - dependencies: - '@apollo/utils.keyvaluecache': 1.0.2 - '@apollo/utils.logger': 1.0.1 - '@apollo/utils.usagereporting': 1.0.1(graphql@16.12.0) - '@apollographql/apollo-tools': 0.5.4(graphql@16.12.0) - '@apollographql/graphql-playground-html': 1.6.29 - '@graphql-tools/mock': 8.7.20(graphql@16.12.0) - '@graphql-tools/schema': 8.5.1(graphql@16.12.0) - '@josephg/resolvable': 1.0.1 - apollo-datasource: 3.3.2(encoding@0.1.13) - apollo-reporting-protobuf: 3.4.0 - apollo-server-env: 4.2.1(encoding@0.1.13) - apollo-server-errors: 3.3.1(graphql@16.12.0) - apollo-server-plugin-base: 3.7.2(encoding@0.1.13)(graphql@16.12.0) - apollo-server-types: 3.8.0(encoding@0.1.13)(graphql@16.12.0) - async-retry: 1.3.3 - fast-json-stable-stringify: 2.1.0 - graphql: 16.12.0 - graphql-tag: 2.12.6(graphql@16.12.0) - loglevel: 1.9.2 - lru-cache: 6.0.0 - node-abort-controller: 3.1.1 - sha.js: 2.4.12 - uuid: 9.0.1 - whatwg-mimetype: 3.0.0 - transitivePeerDependencies: - - encoding - - apollo-server-env@4.2.1(encoding@0.1.13): - dependencies: - node-fetch: 2.6.7(encoding@0.1.13) - transitivePeerDependencies: - - encoding - - apollo-server-errors@3.3.1(graphql@16.12.0): - dependencies: - graphql: 16.12.0 - - apollo-server-express@3.13.0(encoding@0.1.13)(express@4.21.2)(graphql@16.12.0): - dependencies: - '@types/accepts': 1.3.7 - '@types/body-parser': 1.19.2 - '@types/cors': 2.8.12 - '@types/express': 4.17.14 - '@types/express-serve-static-core': 4.17.31 - accepts: 1.3.8 - apollo-server-core: 3.13.0(encoding@0.1.13)(graphql@16.12.0) - apollo-server-types: 3.8.0(encoding@0.1.13)(graphql@16.12.0) - body-parser: 1.20.4 - cors: 2.8.5 - express: 4.21.2 - graphql: 16.12.0 - parseurl: 1.3.3 - transitivePeerDependencies: - - encoding - - supports-color - - apollo-server-plugin-base@3.7.2(encoding@0.1.13)(graphql@16.12.0): - dependencies: - apollo-server-types: 3.8.0(encoding@0.1.13)(graphql@16.12.0) - graphql: 16.12.0 - transitivePeerDependencies: - - encoding - - apollo-server-types@3.8.0(encoding@0.1.13)(graphql@16.12.0): - dependencies: - '@apollo/utils.keyvaluecache': 1.0.2 - '@apollo/utils.logger': 1.0.1 - apollo-reporting-protobuf: 3.4.0 - apollo-server-env: 4.2.1(encoding@0.1.13) - graphql: 16.12.0 - transitivePeerDependencies: - - encoding - append-field@1.0.0: {} aproba@2.0.0: {} @@ -13319,8 +12183,6 @@ snapshots: array-differ@3.0.0: {} - array-flatten@1.1.1: {} - array-ify@1.0.0: {} array-union@2.1.0: {} @@ -13370,57 +12232,8 @@ snapshots: transitivePeerDependencies: - debug - babel-jest@29.7.0(@babel/core@7.28.5): - dependencies: - '@babel/core': 7.28.5 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.28.5) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-istanbul@6.1.1: - dependencies: - '@babel/helper-plugin-utils': 7.27.1 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@29.6.3: - dependencies: - '@babel/template': 7.27.2 - '@babel/types': 7.28.5 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 - babel-plugin-syntax-trailing-function-commas@7.0.0-beta.0: {} - babel-preset-current-node-syntax@1.2.0(@babel/core@7.28.5): - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.28.5) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.28.5) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.28.5) - '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.5) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.28.5) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.28.5) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.28.5) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.5) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.28.5) - babel-preset-fbjs@3.4.0(@babel/core@7.28.5): dependencies: '@babel/core': 7.28.5 @@ -13454,12 +12267,6 @@ snapshots: transitivePeerDependencies: - supports-color - babel-preset-jest@29.6.3(@babel/core@7.28.5): - dependencies: - '@babel/core': 7.28.5 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) - backo2@1.0.2: {} bail@2.0.2: {} @@ -13493,37 +12300,17 @@ snapshots: bmp-js@0.1.0: {} - body-parser@1.20.3: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.13.0 - raw-body: 2.5.2 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - body-parser@1.20.4: + body-parser@2.2.2: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 + debug: 4.4.3(supports-color@5.5.0) http-errors: 2.0.1 - iconv-lite: 0.4.24 + iconv-lite: 0.7.1 on-finished: 2.4.1 qs: 6.14.1 - raw-body: 2.5.3 - type-is: 1.6.18 - unpipe: 1.0.0 + raw-body: 3.0.2 + type-is: 2.0.1 transitivePeerDependencies: - supports-color @@ -13550,10 +12337,6 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -13644,8 +12427,6 @@ snapshots: camelcase@5.3.1: {} - camelcase@6.3.0: {} - caniuse-lite@1.0.30001762: {} canvas@3.2.1: @@ -13701,8 +12482,6 @@ snapshots: snake-case: 3.0.4 tslib: 2.8.1 - char-regex@1.0.2: {} - character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} @@ -13736,10 +12515,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - chokidar@4.0.1: - dependencies: - readdirp: 4.1.2 - chokidar@4.0.3: dependencies: readdirp: 4.1.2 @@ -13756,8 +12531,6 @@ snapshots: dependencies: consola: 3.4.2 - cjs-module-lexer@1.4.3: {} - class-transformer@0.5.1: {} class-validator@0.14.3: @@ -13823,12 +12596,8 @@ snapshots: cmd-shim@6.0.3: {} - co@4.6.0: {} - collapse-white-space@2.1.0: {} - collect-v8-coverage@1.0.3: {} - color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -13880,8 +12649,6 @@ snapshots: confbox@0.2.2: {} - consola@2.15.3: {} - consola@3.4.2: {} console-control-strings@1.1.0: {} @@ -13892,9 +12659,7 @@ snapshots: tslib: 2.8.1 upper-case: 2.0.2 - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 + content-disposition@1.1.0: {} content-type@1.0.5: {} @@ -13959,7 +12724,7 @@ snapshots: cookie-signature@1.0.6: {} - cookie@0.7.1: {} + cookie-signature@1.2.2: {} cookie@0.7.2: {} @@ -13969,7 +12734,7 @@ snapshots: core-util-is@1.0.3: {} - cors@2.8.5: + cors@2.8.6: dependencies: object-assign: 4.1.1 vary: 1.1.2 @@ -13992,21 +12757,6 @@ snapshots: optionalDependencies: typescript: 5.9.3 - create-jest@29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - create-require@1.1.1: {} cross-fetch@3.2.0(encoding@0.1.13): @@ -14041,10 +12791,6 @@ snapshots: debounce@2.2.0: {} - debug@2.6.9: - dependencies: - ms: 2.0.0 - debug@4.4.3(supports-color@5.5.0): dependencies: ms: 2.1.3 @@ -14104,8 +12850,6 @@ snapshots: destr@2.0.5: {} - destroy@1.2.0: {} - detect-indent@5.0.0: {} detect-indent@6.1.0: {} @@ -14114,8 +12858,6 @@ snapshots: detect-libc@2.1.2: {} - detect-newline@3.1.0: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -14144,22 +12886,24 @@ snapshots: dependencies: is-obj: 2.0.0 - dotenv-expand@10.0.0: {} - dotenv-expand@11.0.7: dependencies: - dotenv: 16.4.7 + dotenv: 16.6.1 dotenv-expand@12.0.1: dependencies: - dotenv: 16.4.7 + dotenv: 16.6.1 - dotenv@16.4.5: {} + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 dotenv@16.4.7: {} dotenv@16.6.1: {} + dotenv@17.4.1: {} + dset@3.1.4: {} duck@0.1.12: @@ -14191,8 +12935,6 @@ snapshots: electron-to-chromium@1.5.267: {} - emittery@0.13.1: {} - emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -14201,8 +12943,6 @@ snapshots: empathic@2.0.0: {} - encodeurl@1.0.2: {} - encodeurl@2.0.0: {} encoding@0.1.13: @@ -14329,8 +13069,6 @@ snapshots: escape-string-regexp@1.0.5: {} - escape-string-regexp@2.0.0: {} - escape-string-regexp@5.0.0: {} esprima@4.0.1: {} @@ -14392,54 +13130,41 @@ snapshots: signal-exit: 3.0.7 strip-final-newline: 2.0.0 - exit@0.1.2: {} - expand-template@2.0.3: {} expect-type@1.3.0: {} - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - exponential-backoff@3.1.3: {} - express@4.21.2: + express@5.2.1: dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.3 - content-disposition: 0.5.4 + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 content-type: 1.0.5 - cookie: 0.7.1 - cookie-signature: 1.0.6 - debug: 2.6.9 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@5.5.0) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.1 - fresh: 0.5.2 - http-errors: 2.0.0 - merge-descriptors: 1.0.3 - methods: 1.1.2 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 on-finished: 2.4.1 + once: 1.4.0 parseurl: 1.3.3 - path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.13.0 + qs: 6.14.1 range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.0 - serve-static: 1.16.2 - setprototypeof: 1.2.0 - statuses: 2.0.1 - type-is: 1.6.18 - utils-merge: 1.0.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: - supports-color @@ -14458,14 +13183,6 @@ snapshots: dependencies: pure-rand: 6.1.0 - fast-glob@3.3.2: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -14474,8 +13191,6 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - fast-safe-stringify@2.1.1: {} fast-sha256@1.3.0: {} @@ -14531,16 +13246,16 @@ snapshots: dependencies: tslib: 2.8.1 - file-type@20.4.1: + file-type@21.2.0: dependencies: - '@tokenizer/inflate': 0.2.7 + '@tokenizer/inflate': 0.4.1 strtok3: 10.3.4 token-types: 6.1.2 uint8array-extras: 1.5.0 transitivePeerDependencies: - supports-color - file-type@21.2.0: + file-type@21.3.4: dependencies: '@tokenizer/inflate': 0.4.1 strtok3: 10.3.4 @@ -14557,15 +13272,14 @@ snapshots: dependencies: to-regex-range: 5.0.1 - finalhandler@1.3.1: + finalhandler@2.1.1: dependencies: - debug: 2.6.9 + debug: 4.4.3(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.1 - unpipe: 1.0.0 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -14622,7 +13336,7 @@ snapshots: fraction.js@5.3.4: {} - fresh@0.5.2: {} + fresh@2.0.0: {} front-matter@4.0.2: dependencies: @@ -14677,8 +13391,6 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 - get-package-type@0.1.0: {} - get-pkg-repo@4.2.1: dependencies: '@hutson/parse-repository-url': 3.0.2 @@ -14831,15 +13543,17 @@ snapshots: dependencies: graphql: 16.12.0 - graphql-ws@5.16.0(graphql@16.12.0): + graphql-ws@6.0.6(graphql@16.12.0)(ws@8.18.3): dependencies: graphql: 16.12.0 + optionalDependencies: + ws: 8.18.3 - graphql-ws@6.0.6(graphql@16.12.0)(ws@8.18.3): + graphql-ws@6.0.8(graphql@16.12.0)(ws@8.20.0): dependencies: graphql: 16.12.0 optionalDependencies: - ws: 8.18.3 + ws: 8.20.0 graphql@16.12.0: {} @@ -14949,20 +13663,10 @@ snapshots: dependencies: lru-cache: 10.4.3 - html-escaper@2.0.2: {} - html-url-attributes@3.0.1: {} http-cache-semantics@4.2.0: {} - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -15147,8 +13851,6 @@ snapshots: dependencies: get-east-asian-width: 1.4.0 - is-generator-fn@2.1.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -15167,435 +13869,96 @@ snapshots: is-obj@2.0.0: {} - is-plain-obj@1.1.0: {} - - is-plain-obj@4.1.0: {} - - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - - is-property@1.0.2: {} - - is-relative@1.0.0: - dependencies: - is-unc-path: 1.0.0 - - is-ssh@1.4.1: - dependencies: - protocols: 2.0.2 - - is-stream@2.0.0: {} - - is-text-path@1.0.1: - dependencies: - text-extensions: 1.9.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.19 - - is-unc-path@1.0.0: - dependencies: - unc-path-regex: 0.1.2 - - is-unicode-supported@0.1.0: {} - - is-upper-case@2.0.2: - dependencies: - tslib: 2.8.1 - - is-url@1.2.4: {} - - is-windows@1.0.2: {} - - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - - isarray@1.0.0: {} - - isarray@2.0.5: {} - - isexe@2.0.0: {} - - isexe@3.1.1: {} - - isobject@3.0.1: {} - - isomorphic-ws@5.0.0(ws@8.18.3): - dependencies: - ws: 8.18.3 - - isows@1.0.7(ws@8.18.3): - dependencies: - ws: 8.18.3 - - istanbul-lib-coverage@3.2.2: {} - - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.28.5 - '@babel/parser': 7.28.5 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.28.5 - '@babel/parser': 7.28.5 - '@istanbuljs/schema': 0.1.3 - istanbul-lib-coverage: 3.2.2 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color - - istanbul-lib-report@3.0.1: - dependencies: - istanbul-lib-coverage: 3.2.2 - make-dir: 4.0.0 - supports-color: 7.2.0 - - istanbul-lib-source-maps@4.0.1: - dependencies: - debug: 4.4.3(supports-color@5.5.0) - istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - - istanbul-reports@3.2.0: - dependencies: - html-escaper: 2.0.2 - istanbul-lib-report: 3.0.1 - - iterall@1.3.0: {} - - iterare@1.2.1: {} - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jake@10.9.4: - dependencies: - async: 3.2.6 - filelist: 1.0.4 - picocolors: 1.1.1 - - jest-changed-files@29.7.0: - dependencies: - execa: 5.0.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - - jest-circus@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.5.3 - is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - exit: 0.1.2 - import-local: 3.1.0 - jest-config: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-config@29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.3 - ts-node: 10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 + is-plain-obj@1.1.0: {} + + is-plain-obj@4.1.0: {} - jest-docblock@29.7.0: + is-plain-object@2.0.4: dependencies: - detect-newline: 3.1.0 + isobject: 3.0.1 + + is-promise@4.0.0: {} + + is-property@1.0.2: {} - jest-each@29.7.0: + is-relative@1.0.0: dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 + is-unc-path: 1.0.0 - jest-environment-node@29.7.0: + is-ssh@1.4.1: dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - jest-mock: 29.7.0 - jest-util: 29.7.0 + protocols: 2.0.2 - jest-get-type@29.6.3: {} + is-stream@2.0.0: {} - jest-haste-map@29.7.0: + is-text-path@1.0.1: dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 22.19.3 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 + text-extensions: 1.9.0 - jest-leak-detector@29.7.0: + is-typed-array@1.1.15: dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 + which-typed-array: 1.1.19 - jest-matcher-utils@29.7.0: + is-unc-path@1.0.0: dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 + unc-path-regex: 0.1.2 - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.27.1 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 + is-unicode-supported@0.1.0: {} - jest-mock@29.7.0: + is-upper-case@2.0.2: dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - jest-util: 29.7.0 + tslib: 2.8.1 - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: - jest-resolve: 29.7.0 + is-url@1.2.4: {} - jest-regex-util@29.6.3: {} + is-windows@1.0.2: {} - jest-resolve-dependencies@29.7.0: + is-wsl@2.2.0: dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color + is-docker: 2.2.1 - jest-resolve@29.7.0: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.11 - resolve.exports: 2.0.3 - slash: 3.0.0 + isarray@1.0.0: {} + + isarray@2.0.5: {} - jest-runner@29.7.0: + isexe@2.0.0: {} + + isexe@3.1.1: {} + + isobject@3.0.1: {} + + isomorphic-ws@5.0.0(ws@8.18.3): dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color + ws: 8.18.3 - jest-runtime@29.7.0: + isows@1.0.7(ws@8.18.3): dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - chalk: 4.1.2 - cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.3 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color + ws: 8.18.3 - jest-snapshot@29.7.0: + iterall@1.3.0: {} + + iterare@1.2.1: {} + + jackspeak@3.4.3: dependencies: - '@babel/core': 7.28.5 - '@babel/generator': 7.28.5 - '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) - '@babel/types': 7.28.5 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.28.5) - chalk: 4.1.2 - expect: 29.7.0 - graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 - semver: 7.7.3 - transitivePeerDependencies: - - supports-color + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 - jest-util@29.7.0: + jake@10.9.4: dependencies: - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.1 + async: 3.2.6 + filelist: 1.0.4 + picocolors: 1.1.1 - jest-validate@29.7.0: + jest-diff@29.7.0: dependencies: - '@jest/types': 29.6.3 - camelcase: 6.3.0 chalk: 4.1.2 + diff-sequences: 29.6.3 jest-get-type: 29.6.3 - leven: 3.1.0 pretty-format: 29.7.0 - jest-watcher@29.7.0: - dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 22.19.3 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.7.0 - string-length: 4.0.2 - - jest-worker@29.7.0: - dependencies: - '@types/node': 22.19.3 - jest-util: 29.7.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest@29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node + jest-get-type@29.6.3: {} jiti@2.6.1: {} @@ -15641,19 +14004,6 @@ snapshots: jsonparse@1.3.1: {} - jsonwebtoken@9.0.2: - dependencies: - jws: 3.2.3 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 7.7.3 - jsonwebtoken@9.0.3: dependencies: jws: 4.0.1 @@ -15678,23 +14028,12 @@ snapshots: just-diff@6.0.2: {} - jwa@1.4.2: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - jwa@2.0.1: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jws@3.2.3: - dependencies: - jwa: 1.4.2 - safe-buffer: 5.2.1 - jws@4.0.1: dependencies: jwa: 2.0.1 @@ -15702,8 +14041,6 @@ snapshots: kind-of@6.0.3: {} - kleur@3.0.3: {} - lerna@8.2.4(@swc/core@1.15.13)(@types/node@22.19.3)(encoding@0.1.13): dependencies: '@lerna/create': 8.2.4(@swc/core@1.15.13)(@types/node@22.19.3)(encoding@0.1.13)(typescript@5.9.3) @@ -15795,8 +14132,6 @@ snapshots: - encoding - supports-color - leven@3.1.0: {} - libnpmaccess@8.0.6: dependencies: npm-package-arg: 11.0.2 @@ -15930,9 +14265,7 @@ snapshots: lodash.isstring@4.0.1: {} - lodash.memoize@4.1.2: {} - - lodash.omit@4.5.0: {} + lodash.omit@4.18.0: {} lodash.once@4.1.1: {} @@ -15940,6 +14273,8 @@ snapshots: lodash@4.17.21: {} + lodash@4.18.1: {} + log-symbols@4.1.0: dependencies: chalk: 4.1.2 @@ -15987,6 +14322,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.3.3: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -15995,10 +14332,6 @@ snapshots: dependencies: yallist: 4.0.0 - lru-cache@7.13.1: {} - - lru-cache@7.18.3: {} - lru.min@1.1.3: {} magic-string@0.30.21: @@ -16033,10 +14366,6 @@ snapshots: transitivePeerDependencies: - supports-color - makeerror@1.0.12: - dependencies: - tmpl: 1.0.5 - mammoth@1.11.0: dependencies: '@xmldom/xmldom': 0.8.11 @@ -16227,6 +14556,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + meow@8.1.2: dependencies: '@types/minimist': 1.2.5 @@ -16241,7 +14572,7 @@ snapshots: type-fest: 0.18.1 yargs-parser: 20.2.9 - merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} merge-stream@2.0.0: {} @@ -16524,11 +14855,15 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 - mime@1.6.0: {} + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 mime@2.6.0: {} @@ -16613,10 +14948,6 @@ snapshots: mkdirp-classic@0.5.3: {} - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - mkdirp@1.0.4: {} mlly@1.8.0: @@ -16632,19 +14963,14 @@ snapshots: mrmime@2.0.1: {} - ms@2.0.0: {} - ms@2.1.3: {} - multer@2.0.2: + multer@2.1.1: dependencies: append-field: 1.0.0 busboy: 1.6.0 concat-stream: 2.0.0 - mkdirp: 0.5.6 - object-assign: 4.1.1 type-is: 1.6.18 - xtend: 4.0.2 multimatch@5.0.0: dependencies: @@ -16652,7 +14978,7 @@ snapshots: array-differ: 3.0.0 array-union: 2.1.0 arrify: 2.0.1 - minimatch: 3.0.5 + minimatch: 3.1.2 mute-stream@0.0.8: {} @@ -16686,20 +15012,18 @@ snapshots: napi-build-utils@2.0.0: {} - natural-compare@1.4.0: {} - - negotiator@0.6.3: {} - negotiator@0.6.4: {} + negotiator@1.0.0: {} + neo-async@2.6.2: {} - nest-commander@3.16.0(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@types/inquirer@8.2.12)(typescript@5.9.3): + nest-commander@3.16.0(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(@types/inquirer@8.2.12)(typescript@5.9.3): dependencies: '@fig/complete-commander': 3.2.0(commander@11.1.0) - '@golevelup/nestjs-discovery': 4.0.3(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) - '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@golevelup/nestjs-discovery': 4.0.3(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.1.18(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.18)(reflect-metadata@0.2.2)(rxjs@7.8.2) '@types/inquirer': 8.2.12 commander: 11.1.0 cosmiconfig: 8.3.6(typescript@5.9.3) @@ -16707,13 +15031,17 @@ snapshots: transitivePeerDependencies: - typescript - nestjs-zod@5.1.1(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)(zod@4.3.6): + nestjs-zod@5.1.1(@nestjs/common@11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)(zod@4.3.6): dependencies: - '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/common': 11.1.18(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) deepmerge: 4.3.1 rxjs: 7.8.2 zod: 4.3.6 + neverthrow@8.2.0: + optionalDependencies: + '@rollup/rollup-linux-x64-gnu': 4.54.0 + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -16723,8 +15051,6 @@ snapshots: dependencies: semver: 7.7.3 - node-abort-controller@3.1.1: {} - node-addon-api@7.1.1: {} node-domexception@1.0.0: {} @@ -17159,9 +15485,7 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 - path-to-regexp@0.1.12: {} - - path-to-regexp@3.3.0: {} + path-to-regexp@8.4.2: {} path-type@3.0.0: dependencies: @@ -17361,11 +15685,6 @@ snapshots: dependencies: asap: 2.0.6 - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - promzard@1.0.2: dependencies: read: 3.0.1 @@ -17402,10 +15721,6 @@ snapshots: pure-rand@6.1.0: {} - qs@6.13.0: - dependencies: - side-channel: 1.1.0 - qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -17418,18 +15733,11 @@ snapshots: range-parser@1.2.1: {} - raw-body@2.5.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.0 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - - raw-body@2.5.3: + raw-body@3.0.2: dependencies: bytes: 3.1.2 http-errors: 2.0.1 - iconv-lite: 0.4.24 + iconv-lite: 0.7.1 unpipe: 1.0.0 rc9@2.1.2: @@ -17760,6 +16068,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.54.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + run-async@2.4.1: {} run-parallel@1.2.0: @@ -17784,21 +16102,19 @@ snapshots: semver@7.7.3: {} - send@0.19.0: + send@1.2.1: dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 + debug: 4.4.3(supports-color@5.5.0) + encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.1 + statuses: 2.0.2 transitivePeerDependencies: - supports-color @@ -17810,12 +16126,12 @@ snapshots: seq-queue@0.0.5: {} - serve-static@1.16.2: + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.0 + send: 1.2.1 transitivePeerDependencies: - supports-color @@ -17919,8 +16235,6 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 - sisteransi@1.0.5: {} - slash@3.0.0: {} slice-ansi@7.1.2: @@ -17954,11 +16268,6 @@ snapshots: source-map-js@1.2.1: {} - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - source-map@0.6.1: {} source-map@0.7.6: {} @@ -18001,14 +16310,8 @@ snapshots: dependencies: minipass: 7.1.2 - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - stackback@0.0.2: {} - statuses@2.0.1: {} - statuses@2.0.2: {} std-env@3.10.0: {} @@ -18019,11 +16322,6 @@ snapshots: string-env-interpolation@1.0.1: {} - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -18080,8 +16378,6 @@ snapshots: strip-json-comments@2.0.1: {} - strip-json-comments@3.1.1: {} - strnum@2.2.1: {} strtok3@10.3.4: @@ -18148,10 +16444,6 @@ snapshots: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} svix@1.76.1: @@ -18224,12 +16516,6 @@ snapshots: transitivePeerDependencies: - encoding - test-exclude@6.0.0: - dependencies: - '@istanbuljs/schema': 0.1.3 - glob: 7.2.3 - minimatch: 3.0.5 - text-extensions@1.9.0: {} thenify-all@1.6.0: @@ -18277,8 +16563,6 @@ snapshots: tmp@0.2.5: {} - tmpl@1.0.5: {} - to-buffer@1.2.2: dependencies: isarray: 2.0.5 @@ -18315,26 +16599,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.6(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.8 - jest: 29.7.0(@types/node@22.19.3)(ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.7.3 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 - optionalDependencies: - '@babel/core': 7.28.5 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.28.5) - jest-util: 29.7.0 - ts-log@2.2.7: {} ts-node@10.9.2(@swc/core@1.15.13)(@types/node@22.19.3)(typescript@5.9.3): @@ -18410,7 +16674,9 @@ snapshots: dependencies: safe-buffer: 5.2.1 - type-detect@4.0.8: {} + turndown@7.2.4: + dependencies: + '@mixmark-io/domino': 2.2.0 type-fest@0.18.1: {} @@ -18429,6 +16695,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typed-array-buffer@1.0.3: dependencies: call-bound: 1.0.4 @@ -18551,16 +16823,10 @@ snapshots: uuid@11.0.3: {} - uuid@9.0.1: {} + uuid@11.1.0: {} v8-compile-cache-lib@3.0.1: {} - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - valibot@1.2.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -18574,10 +16840,6 @@ snapshots: validator@13.15.26: {} - value-or-promise@1.0.11: {} - - value-or-promise@1.0.12: {} - vary@1.1.2: {} vfile-message@4.0.3: @@ -18655,10 +16917,6 @@ snapshots: walk-up-path@3.0.1: {} - walker@1.0.8: - dependencies: - makeerror: 1.0.12 - wasm-feature-detect@1.8.0: {} wcwidth@1.0.1: @@ -18669,8 +16927,6 @@ snapshots: webidl-conversions@3.0.1: {} - whatwg-mimetype@3.0.0: {} - whatwg-mimetype@4.0.0: {} whatwg-url@5.0.0: @@ -18741,11 +16997,6 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - write-file-atomic@5.0.1: dependencies: imurmurhash: 0.1.4 @@ -18768,10 +17019,10 @@ snapshots: ws@7.5.10: {} - ws@8.18.0: {} - ws@8.18.3: {} + ws@8.20.0: {} + xmlbuilder@10.1.1: {} xss@1.0.15: