diff --git a/server/codegen.yml b/server/codegen.yml index b792f0f..bed6fd7 100644 --- a/server/codegen.yml +++ b/server/codegen.yml @@ -2,19 +2,19 @@ overwrite: true schema: 'schema.graphql' documents: null generates: - src/generated/resolver-types.ts: + src/shared/infra/http/generated/resolver-types.ts: plugins: - 'typescript' - 'typescript-resolvers' config: mapperTypeSuffix: Model mappers: - ListAllMembersResponse: '@/members/list-all-members/listAllMembersService#ListAllMembersResponse' - ShowMemberDetailResponse: '@/members/show-member-detail/showMemberDetailService#ShowMemberDetailResponse' - UserInfo: '@/users/get-logged-in-user-info/getLoggedInUserInfoService#GetLoggedInUserInfoResponse' - UserMenuItem: '@/users/get-logged-in-user-info/getLoggedInUserInfoService#UserMenuItem' - Member: '@/members/shared/member#DisplayableMember' - Department: '@/members/shared/department#Department' - EditMemberDetailInput: '@/members/edit-member-detail/editMemberDetailService#EditMemberDetailRequest' - EditMemberDetailResponse: '@/members/edit-member-detail/editMemberDetailService#EditMemberDetailResponse' - contextType: '@/context#Context' + ListAllMembersResponse: '@/modules/members/useCases/query/listAllMembers/listAllMembersService#ListAllMembersResponse' + ShowMemberDetailResponse: '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService#ShowMemberDetailResponse' + UserInfo: '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService#GetLoggedInUserInfoResponse' + UserMenuItem: '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService#UserMenuItem' + Member: '@/modules/members/dtos/displayableMemberDTO#DisplayableMember' + Department: '@/modules/members/dtos/departmentDTO#DepartmentDTO' + EditMemberDetailInput: '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService#EditMemberDetailRequest' + EditMemberDetailResponse: '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService#EditMemberDetailResponse' + contextType: '@/shared/infra/http/context#Context' diff --git a/server/package-lock.json b/server/package-lock.json index 2322333..39855a6 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,13 +12,14 @@ "dependencies": { "@graphql-tools/graphql-file-loader": "^7.3.10", "@graphql-tools/load": "^7.5.9", - "@graphql-yoga/node": "^2.3.0", "@prisma/client": "^3.12.0", "express": "^4.17.1", "graphql": "^16.3.0", + "graphql-yoga": "^3.4.0", "oso": "0.25.0", "reflect-metadata": "^0.1.13", - "sqlite3": "^5.0.2" + "sqlite3": "^5.0.2", + "zod": "^3.20.2" }, "devDependencies": { "@faker-js/faker": "^6.2.0", @@ -32,6 +33,7 @@ "@types/node": "^16.11.27", "@types/supertest": "^2.0.11", "@types/yargs": "^17.0.4", + "fishery": "^2.2.2", "jest": "^27.3.1", "nodemon": "^2.0.14", "prisma": "^3.12.0", @@ -2772,6 +2774,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/@envelop/core/-/core-2.3.1.tgz", "integrity": "sha512-AnYUci7EGyA8flml881lDvVDl6n/u6GQpVIOTsZVO29d4/rPqSJ2KFguDD3mjDL406doTTLNuDI4ndxfJl6fmQ==", + "dev": true, "dependencies": { "@envelop/types": "2.2.0" }, @@ -2783,6 +2786,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@envelop/disable-introspection/-/disable-introspection-3.3.1.tgz", "integrity": "sha512-THR8UnRQQB5nCLmITXvebwXwSHvFjS+ThA3RRVXpFX9EupMbTFN5a4NHty7+BYD798c3VrBZ/InbMlEWqw1c9g==", + "dev": true, "peerDependencies": { "@envelop/core": "^2.3.1", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" @@ -2792,6 +2796,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@envelop/parser-cache/-/parser-cache-4.3.1.tgz", "integrity": "sha512-IqerCVjvVTiGvSZ8qSpdEc55hhiuekufJd0+ldWtyMPznhMaYOzpLifFUhjhhD7004eJM17n9vjJQFa7fIGz8Q==", + "dev": true, "dependencies": { "tiny-lru": "7.0.6" }, @@ -2804,6 +2809,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@envelop/types/-/types-2.2.0.tgz", "integrity": "sha512-Lghvfs0kh53G5mUKpCMlB/FhHh3O8SSR4hewB7JyE9hOEu/9h/6u+GHH/OEgdaRHky1Sae5Jf4grO+h21ka4ig==", + "dev": true, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } @@ -2812,6 +2818,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@envelop/validation-cache/-/validation-cache-4.3.1.tgz", "integrity": "sha512-lmtu9idhdWqbYkcFoFsL1ED4y38DLvj6EDEwE9tULXWuZm4WWmlNQAmKHAwB1d3kGVQAMtxM59crkOOJGRBgHQ==", + "dev": true, "dependencies": { "tiny-lru": "7.0.6" }, @@ -3078,6 +3085,45 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/@graphql-tools/executor": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.12.tgz", + "integrity": "sha512-bWpZcYRo81jDoTVONTnxS9dDHhEkNVjxzvFCH4CRpuyzD3uL+5w3MhtxIh24QyWm4LvQ4f+Bz3eMV2xU2I5+FA==", + "dependencies": { + "@graphql-tools/utils": "9.1.4", + "@graphql-typed-document-node/core": "3.1.1", + "@repeaterjs/repeater": "3.0.4", + "tslib": "^2.4.0", + "value-or-promise": "1.0.12" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor/node_modules/@graphql-tools/utils": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.1.4.tgz", + "integrity": "sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@graphql-tools/executor/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/@graphql-tools/executor/node_modules/value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/@graphql-tools/git-loader": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-7.1.12.tgz", @@ -3642,6 +3688,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/common/-/common-2.3.0.tgz", "integrity": "sha512-EPKK97953c8E1FiaLHMMGqLKtoAN5H9qHr0AiAzMlruJHn0JcbMf2qFTUXklCsuk6UEwNtxeHX42zim11O/E1g==", + "dev": true, "dependencies": { "@envelop/core": "^2.0.0", "@envelop/disable-introspection": "^3.0.0", @@ -3663,6 +3710,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/node/-/node-2.3.0.tgz", "integrity": "sha512-uofEmKIDYthJuCcdhbgU0VW5i2cCqZVKIiEW/xbwvCOBJt439k46D+M6youiQYJ1miaA/m0btbuZ1sAo/TLjdQ==", + "dev": true, "dependencies": { "@envelop/core": "^2.0.0", "@graphql-tools/utils": "^8.6.0", @@ -3679,6 +3727,16 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-2.0.0.tgz", "integrity": "sha512-HlG+gIddjIP3/BDrMZymdzmzDjNdYuSGMxx6+1JA83gAEVRDR4yOoT4QeNKYqRhLK9xca/Hxp1PfBpquSa244Q==", + "dev": true, + "dependencies": { + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.3.1" + } + }, + "node_modules/@graphql-yoga/typed-event-target": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-1.0.0.tgz", + "integrity": "sha512-Mqni6AEvl3VbpMtKw+TIjc9qS9a8hKhiAjFtqX488yq5oJtj9TkNlFTIacAVS3vnPiswNsmDiQqvwUOcJgi1DA==", "dependencies": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.3.1" @@ -4051,6 +4109,52 @@ "node": ">= 8" } }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz", + "integrity": "sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ==", + "dependencies": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@peculiar/asn1-schema/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz", + "integrity": "sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.0", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.1", + "webcrypto-core": "^1.7.4" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/@peculiar/webcrypto/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/@prisma/client": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.12.0.tgz", @@ -4075,7 +4179,7 @@ "version": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz", "integrity": "sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ==", - "devOptional": true, + "dev": true, "hasInstallScript": true }, "node_modules/@prisma/engines-version": { @@ -4440,6 +4544,39 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, + "node_modules/@whatwg-node/events": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.2.tgz", + "integrity": "sha512-WKj/lI4QjnLuPrim0cfO7i+HsDSXHxNv1y0CrJhdntuO3hxWZmnXCwNDnwOvry11OjRin6cgWNF+j/9Pn8TN4w==" + }, + "node_modules/@whatwg-node/fetch": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.6.2.tgz", + "integrity": "sha512-fCUycF1W+bI6XzwJFnbdDuxIldfKM3w8+AzVCLGlucm0D+AQ8ZMm2j84hdcIhfV6ZdE4Y1HFVrHosAxdDZ+nPw==", + "dependencies": { + "@peculiar/webcrypto": "^1.4.0", + "abort-controller": "^3.0.0", + "busboy": "^1.6.0", + "form-data-encoder": "^1.7.1", + "formdata-node": "^4.3.1", + "node-fetch": "^2.6.7", + "undici": "^5.12.0", + "urlpattern-polyfill": "^6.0.2", + "web-streams-polyfill": "^3.2.0" + } + }, + "node_modules/@whatwg-node/server": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@whatwg-node/server/-/server-0.5.8.tgz", + "integrity": "sha512-29f2Ijk663Hr6hF5GU5a8ELGQVbNMMDBWF1lTdpIKGyLrLJTKixarp6COEyEN5H9tGzIRUQar9Z76A+Jb9DyzQ==", + "dependencies": { + "@whatwg-node/fetch": "0.6.2", + "tslib": "^2.3.1" + }, + "peerDependencies": { + "@types/node": "^18.0.6" + } + }, "node_modules/abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -4712,6 +4849,24 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "dependencies": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/asn1js/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -5158,6 +5313,17 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -5773,6 +5939,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/cross-undici-fetch/-/cross-undici-fetch-0.2.5.tgz", "integrity": "sha512-6IR+JN6o2UMNj2f3fu0ZVkZeP0h22DRKzq78SiMenkqyBYyGIT1AkZjHkItvh0A80LdsAlWENHUpvapapePucw==", + "dev": true, "dependencies": { "abort-controller": "^3.0.0", "form-data-encoder": "^1.7.1", @@ -6575,6 +6742,15 @@ "node": ">=8" } }, + "node_modules/fishery": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fishery/-/fishery-2.2.2.tgz", + "integrity": "sha512-jeU0nDhPHJkupmjX+r9niKgVMTBDB8X+U/pktoGHAiWOSyNlMd0HhmqnjrpjUOCDPJYaSSu4Ze16h6dZOKSp2w==", + "dev": true, + "dependencies": { + "lodash.mergewith": "^4.6.2" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -7054,6 +7230,131 @@ "graphql": ">=0.11 <=16" } }, + "node_modules/graphql-yoga": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-3.4.0.tgz", + "integrity": "sha512-Cjx60mmpoK1qL/sLdM285VdAOQyJBKLuC6oMZrfO8QleneNtu0nDOM6Efv5m0IrRYSONEMtIYA7eNr0u/cCBfg==", + "dependencies": { + "@envelop/core": "3.0.4", + "@envelop/parser-cache": "^5.0.4", + "@envelop/validation-cache": "^5.0.5", + "@graphql-tools/executor": "0.0.12", + "@graphql-tools/schema": "^9.0.0", + "@graphql-tools/utils": "^9.0.1", + "@graphql-yoga/subscription": "^3.1.0", + "@whatwg-node/fetch": "0.6.2", + "@whatwg-node/server": "0.5.8", + "dset": "^3.1.1", + "tslib": "^2.3.1" + }, + "peerDependencies": { + "graphql": "^15.2.0 || ^16.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@envelop/core": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-3.0.4.tgz", + "integrity": "sha512-AybIZxQsDlFQTWHy6YtX/MSQPVuw+eOFtTW90JsHn6EbmcQnD6N3edQfSiTGjggPRHLoC0+0cuYXp2Ly2r3vrQ==", + "dependencies": { + "@envelop/types": "3.0.1", + "tslib": "2.4.0" + } + }, + "node_modules/graphql-yoga/node_modules/@envelop/parser-cache": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@envelop/parser-cache/-/parser-cache-5.0.4.tgz", + "integrity": "sha512-+kp6nzCVLYI2WQExQcE3FSy6n9ZGB5GYi+ntyjYdxaXU41U1f8RVwiLdyh0Ewn5D/s/zaLin09xkFKITVSAKDw==", + "dependencies": { + "lru-cache": "^6.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@envelop/core": "^3.0.4", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@envelop/types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-3.0.1.tgz", + "integrity": "sha512-Ok62K1K+rlS+wQw77k8Pis8+1/h7+/9Wk5Fgcc2U6M5haEWsLFAHcHsk8rYlnJdEUl2Y3yJcCSOYbt1dyTaU5w==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/graphql-yoga/node_modules/@envelop/validation-cache": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@envelop/validation-cache/-/validation-cache-5.0.5.tgz", + "integrity": "sha512-69sq5H7hvxE+7VV60i0bgnOiV1PX9GEJHKrBrVvyEZAXqYojKO3DP9jnLGryiPgVaBjN5yw12ge0l0s2gXbolQ==", + "dependencies": { + "lru-cache": "^6.0.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@envelop/core": "^3.0.4", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@graphql-tools/merge": { + "version": "8.3.16", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.16.tgz", + "integrity": "sha512-In0kcOZcPIpYOKaqdrJ3thdLPE7TutFnL9tbrHUy2zCinR2O/blpRC48jPckcs0HHrUQ0pGT4HqvzMkZUeEBAw==", + "dependencies": { + "@graphql-tools/utils": "9.1.4", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@graphql-tools/schema": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.14.tgz", + "integrity": "sha512-U6k+HY3Git+dsOEhq+dtWQwYg2CAgue8qBvnBXoKu5eEeH284wymMUoNm0e4IycOgMCJANVhClGEBIkLRu3FQQ==", + "dependencies": { + "@graphql-tools/merge": "8.3.16", + "@graphql-tools/utils": "9.1.4", + "tslib": "^2.4.0", + "value-or-promise": "1.0.12" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@graphql-tools/utils": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.1.4.tgz", + "integrity": "sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==", + "dependencies": { + "tslib": "^2.4.0" + }, + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/graphql-yoga/node_modules/@graphql-yoga/subscription": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-3.1.0.tgz", + "integrity": "sha512-Vc9lh8KzIHyS3n4jBlCbz7zCjcbtQnOBpsymcRvHhFr2cuH+knmRn0EmzimMQ58jQ8kxoRXXC3KJS3RIxSdPIg==", + "dependencies": { + "@graphql-yoga/typed-event-target": "^1.0.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/events": "0.0.2", + "tslib": "^2.3.1" + } + }, + "node_modules/graphql-yoga/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/graphql-yoga/node_modules/value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", + "engines": { + "node": ">=12" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -9318,6 +9619,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -9492,7 +9799,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10609,7 +10915,7 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.12.0.tgz", "integrity": "sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "dependencies": { "@prisma/engines": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" @@ -10704,6 +11010,27 @@ "node": ">=8" } }, + "node_modules/pvtsutils": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.2.tgz", + "integrity": "sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/pvtsutils/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -11484,6 +11811,14 @@ "node": ">= 0.6" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -11855,6 +12190,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==", + "dev": true, "engines": { "node": ">=6" } @@ -12216,9 +12552,12 @@ "dev": true }, "node_modules/undici": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.0.0.tgz", - "integrity": "sha512-VhUpiZ3No1DOPPQVQnsDZyfcbTTcHdcgWej1PdFnSvOeJmOVDgiOHkunJmBLfmjt4CqgPQddPVjSWW0dsTs5Yg==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.16.0.tgz", + "integrity": "sha512-KWBOXNv6VX+oJQhchXieUznEmnJMqgXMbs0xxH2t8q/FUAWSJvOSr/rMaZKnX5RIVq7JDn0JbP4BOnKG2SGXLQ==", + "dependencies": { + "busboy": "^1.6.0" + }, "engines": { "node": ">=12.18" } @@ -12356,6 +12695,14 @@ "node": ">=4" } }, + "node_modules/urlpattern-polyfill": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-6.0.2.tgz", + "integrity": "sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg==", + "dependencies": { + "braces": "^3.0.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12497,6 +12844,23 @@ "node": ">= 8" } }, + "node_modules/webcrypto-core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.5.tgz", + "integrity": "sha512-gaExY2/3EHQlRNNNVSrbG2Cg94Rutl7fAaKILS1w8ZDhGxdFOaw6EbCfHIxPy9vt/xwp5o0VQAx9aySPF6hU1A==", + "dependencies": { + "@peculiar/asn1-schema": "^2.1.6", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.1", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + } + }, + "node_modules/webcrypto-core/node_modules/tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, "node_modules/webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -12676,8 +13040,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "1.10.2", @@ -12740,6 +13103,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } }, "dependencies": { @@ -14781,6 +15152,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/@envelop/core/-/core-2.3.1.tgz", "integrity": "sha512-AnYUci7EGyA8flml881lDvVDl6n/u6GQpVIOTsZVO29d4/rPqSJ2KFguDD3mjDL406doTTLNuDI4ndxfJl6fmQ==", + "dev": true, "requires": { "@envelop/types": "2.2.0" } @@ -14789,12 +15161,13 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@envelop/disable-introspection/-/disable-introspection-3.3.1.tgz", "integrity": "sha512-THR8UnRQQB5nCLmITXvebwXwSHvFjS+ThA3RRVXpFX9EupMbTFN5a4NHty7+BYD798c3VrBZ/InbMlEWqw1c9g==", - "requires": {} + "dev": true }, "@envelop/parser-cache": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@envelop/parser-cache/-/parser-cache-4.3.1.tgz", "integrity": "sha512-IqerCVjvVTiGvSZ8qSpdEc55hhiuekufJd0+ldWtyMPznhMaYOzpLifFUhjhhD7004eJM17n9vjJQFa7fIGz8Q==", + "dev": true, "requires": { "tiny-lru": "7.0.6" } @@ -14803,12 +15176,13 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@envelop/types/-/types-2.2.0.tgz", "integrity": "sha512-Lghvfs0kh53G5mUKpCMlB/FhHh3O8SSR4hewB7JyE9hOEu/9h/6u+GHH/OEgdaRHky1Sae5Jf4grO+h21ka4ig==", - "requires": {} + "dev": true }, "@envelop/validation-cache": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@envelop/validation-cache/-/validation-cache-4.3.1.tgz", "integrity": "sha512-lmtu9idhdWqbYkcFoFsL1ED4y38DLvj6EDEwE9tULXWuZm4WWmlNQAmKHAwB1d3kGVQAMtxM59crkOOJGRBgHQ==", + "dev": true, "requires": { "tiny-lru": "7.0.6" } @@ -15022,6 +15396,38 @@ "value-or-promise": "1.0.11" } }, + "@graphql-tools/executor": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.12.tgz", + "integrity": "sha512-bWpZcYRo81jDoTVONTnxS9dDHhEkNVjxzvFCH4CRpuyzD3uL+5w3MhtxIh24QyWm4LvQ4f+Bz3eMV2xU2I5+FA==", + "requires": { + "@graphql-tools/utils": "9.1.4", + "@graphql-typed-document-node/core": "3.1.1", + "@repeaterjs/repeater": "3.0.4", + "tslib": "^2.4.0", + "value-or-promise": "1.0.12" + }, + "dependencies": { + "@graphql-tools/utils": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.1.4.tgz", + "integrity": "sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==", + "requires": { + "tslib": "^2.4.0" + } + }, + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + }, + "value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==" + } + } + }, "@graphql-tools/git-loader": { "version": "7.1.12", "resolved": "https://registry.npmjs.org/@graphql-tools/git-loader/-/git-loader-7.1.12.tgz", @@ -15421,8 +15827,7 @@ "version": "8.5.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -15450,13 +15855,13 @@ "@graphql-typed-document-node/core": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz", - "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==", - "requires": {} + "integrity": "sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==" }, "@graphql-yoga/common": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/common/-/common-2.3.0.tgz", "integrity": "sha512-EPKK97953c8E1FiaLHMMGqLKtoAN5H9qHr0AiAzMlruJHn0JcbMf2qFTUXklCsuk6UEwNtxeHX42zim11O/E1g==", + "dev": true, "requires": { "@envelop/core": "^2.0.0", "@envelop/disable-introspection": "^3.0.0", @@ -15475,6 +15880,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/node/-/node-2.3.0.tgz", "integrity": "sha512-uofEmKIDYthJuCcdhbgU0VW5i2cCqZVKIiEW/xbwvCOBJt439k46D+M6youiQYJ1miaA/m0btbuZ1sAo/TLjdQ==", + "dev": true, "requires": { "@envelop/core": "^2.0.0", "@graphql-tools/utils": "^8.6.0", @@ -15488,6 +15894,16 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-2.0.0.tgz", "integrity": "sha512-HlG+gIddjIP3/BDrMZymdzmzDjNdYuSGMxx6+1JA83gAEVRDR4yOoT4QeNKYqRhLK9xca/Hxp1PfBpquSa244Q==", + "dev": true, + "requires": { + "@repeaterjs/repeater": "^3.0.4", + "tslib": "^2.3.1" + } + }, + "@graphql-yoga/typed-event-target": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/typed-event-target/-/typed-event-target-1.0.0.tgz", + "integrity": "sha512-Mqni6AEvl3VbpMtKw+TIjc9qS9a8hKhiAjFtqX488yq5oJtj9TkNlFTIacAVS3vnPiswNsmDiQqvwUOcJgi1DA==", "requires": { "@repeaterjs/repeater": "^3.0.4", "tslib": "^2.3.1" @@ -15762,8 +16178,7 @@ "version": "0.9.0", "resolved": "https://registry.npmjs.org/@n1ru4l/graphql-live-query/-/graphql-live-query-0.9.0.tgz", "integrity": "sha512-BTpWy1e+FxN82RnLz4x1+JcEewVdfmUhV1C6/XYD5AjS7PQp9QFF7K8bCD6gzPTr2l+prvqOyVueQhFJxB1vfg==", - "dev": true, - "requires": {} + "dev": true }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -15788,6 +16203,50 @@ "fastq": "^1.6.0" } }, + "@peculiar/asn1-schema": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz", + "integrity": "sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ==", + "requires": { + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, + "@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "requires": { + "tslib": "^2.0.0" + } + }, + "@peculiar/webcrypto": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz", + "integrity": "sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==", + "requires": { + "@peculiar/asn1-schema": "^2.3.0", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.1", + "webcrypto-core": "^1.7.4" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, "@prisma/client": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-3.12.0.tgz", @@ -15800,7 +16259,7 @@ "version": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980", "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980.tgz", "integrity": "sha512-zULjkN8yhzS7B3yeEz4aIym4E2w1ChrV12i14pht3ePFufvsAvBSoZ+tuXMvfSoNTgBS5E4bolRzLbMmbwkkMQ==", - "devOptional": true + "dev": true }, "@prisma/engines-version": { "version": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980", @@ -16144,6 +16603,36 @@ "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", "dev": true }, + "@whatwg-node/events": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.2.tgz", + "integrity": "sha512-WKj/lI4QjnLuPrim0cfO7i+HsDSXHxNv1y0CrJhdntuO3hxWZmnXCwNDnwOvry11OjRin6cgWNF+j/9Pn8TN4w==" + }, + "@whatwg-node/fetch": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.6.2.tgz", + "integrity": "sha512-fCUycF1W+bI6XzwJFnbdDuxIldfKM3w8+AzVCLGlucm0D+AQ8ZMm2j84hdcIhfV6ZdE4Y1HFVrHosAxdDZ+nPw==", + "requires": { + "@peculiar/webcrypto": "^1.4.0", + "abort-controller": "^3.0.0", + "busboy": "^1.6.0", + "form-data-encoder": "^1.7.1", + "formdata-node": "^4.3.1", + "node-fetch": "^2.6.7", + "undici": "^5.12.0", + "urlpattern-polyfill": "^6.0.2", + "web-streams-polyfill": "^3.2.0" + } + }, + "@whatwg-node/server": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@whatwg-node/server/-/server-0.5.8.tgz", + "integrity": "sha512-29f2Ijk663Hr6hF5GU5a8ELGQVbNMMDBWF1lTdpIKGyLrLJTKixarp6COEyEN5H9tGzIRUQar9Z76A+Jb9DyzQ==", + "requires": { + "@whatwg-node/fetch": "0.6.2", + "tslib": "^2.3.1" + } + }, "abab": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", @@ -16353,6 +16842,23 @@ "safer-buffer": "~2.1.0" } }, + "asn1js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", + "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "requires": { + "pvtsutils": "^1.3.2", + "pvutils": "^1.1.3", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -16699,6 +17205,14 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -17195,6 +17709,7 @@ "version": "0.2.5", "resolved": "https://registry.npmjs.org/cross-undici-fetch/-/cross-undici-fetch-0.2.5.tgz", "integrity": "sha512-6IR+JN6o2UMNj2f3fu0ZVkZeP0h22DRKzq78SiMenkqyBYyGIT1AkZjHkItvh0A80LdsAlWENHUpvapapePucw==", + "dev": true, "requires": { "abort-controller": "^3.0.0", "form-data-encoder": "^1.7.1", @@ -17827,6 +18342,15 @@ "path-exists": "^4.0.0" } }, + "fishery": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fishery/-/fishery-2.2.2.tgz", + "integrity": "sha512-jeU0nDhPHJkupmjX+r9niKgVMTBDB8X+U/pktoGHAiWOSyNlMd0HhmqnjrpjUOCDPJYaSSu4Ze16h6dZOKSp2w==", + "dev": true, + "requires": { + "lodash.mergewith": "^4.6.2" + } + }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -18138,8 +18662,7 @@ "version": "0.0.23", "resolved": "https://registry.npmjs.org/graphql-executor/-/graphql-executor-0.0.23.tgz", "integrity": "sha512-3Ivlyfjaw3BWmGtUSnMpP/a4dcXCp0mJtj0PiPG14OKUizaMKlSEX+LX2Qed0LrxwniIwvU6B4w/koVjEPyWJg==", - "dev": true, - "requires": {} + "dev": true }, "graphql-request": { "version": "4.2.0", @@ -18184,8 +18707,111 @@ "version": "5.7.0", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.7.0.tgz", "integrity": "sha512-8yYuvnyqIjlJ/WfebOyu2GSOQeFauRxnfuTveY9yvrDGs2g3kR9Nv4gu40AKvRHbXlSJwTbMJ6dVxAtEyKwVRA==", - "dev": true, - "requires": {} + "dev": true + }, + "graphql-yoga": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/graphql-yoga/-/graphql-yoga-3.4.0.tgz", + "integrity": "sha512-Cjx60mmpoK1qL/sLdM285VdAOQyJBKLuC6oMZrfO8QleneNtu0nDOM6Efv5m0IrRYSONEMtIYA7eNr0u/cCBfg==", + "requires": { + "@envelop/core": "3.0.4", + "@envelop/parser-cache": "^5.0.4", + "@envelop/validation-cache": "^5.0.5", + "@graphql-tools/executor": "0.0.12", + "@graphql-tools/schema": "^9.0.0", + "@graphql-tools/utils": "^9.0.1", + "@graphql-yoga/subscription": "^3.1.0", + "@whatwg-node/fetch": "0.6.2", + "@whatwg-node/server": "0.5.8", + "dset": "^3.1.1", + "tslib": "^2.3.1" + }, + "dependencies": { + "@envelop/core": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@envelop/core/-/core-3.0.4.tgz", + "integrity": "sha512-AybIZxQsDlFQTWHy6YtX/MSQPVuw+eOFtTW90JsHn6EbmcQnD6N3edQfSiTGjggPRHLoC0+0cuYXp2Ly2r3vrQ==", + "requires": { + "@envelop/types": "3.0.1", + "tslib": "2.4.0" + } + }, + "@envelop/parser-cache": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@envelop/parser-cache/-/parser-cache-5.0.4.tgz", + "integrity": "sha512-+kp6nzCVLYI2WQExQcE3FSy6n9ZGB5GYi+ntyjYdxaXU41U1f8RVwiLdyh0Ewn5D/s/zaLin09xkFKITVSAKDw==", + "requires": { + "lru-cache": "^6.0.0", + "tslib": "^2.4.0" + } + }, + "@envelop/types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@envelop/types/-/types-3.0.1.tgz", + "integrity": "sha512-Ok62K1K+rlS+wQw77k8Pis8+1/h7+/9Wk5Fgcc2U6M5haEWsLFAHcHsk8rYlnJdEUl2Y3yJcCSOYbt1dyTaU5w==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@envelop/validation-cache": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@envelop/validation-cache/-/validation-cache-5.0.5.tgz", + "integrity": "sha512-69sq5H7hvxE+7VV60i0bgnOiV1PX9GEJHKrBrVvyEZAXqYojKO3DP9jnLGryiPgVaBjN5yw12ge0l0s2gXbolQ==", + "requires": { + "lru-cache": "^6.0.0", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/merge": { + "version": "8.3.16", + "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.3.16.tgz", + "integrity": "sha512-In0kcOZcPIpYOKaqdrJ3thdLPE7TutFnL9tbrHUy2zCinR2O/blpRC48jPckcs0HHrUQ0pGT4HqvzMkZUeEBAw==", + "requires": { + "@graphql-tools/utils": "9.1.4", + "tslib": "^2.4.0" + } + }, + "@graphql-tools/schema": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.14.tgz", + "integrity": "sha512-U6k+HY3Git+dsOEhq+dtWQwYg2CAgue8qBvnBXoKu5eEeH284wymMUoNm0e4IycOgMCJANVhClGEBIkLRu3FQQ==", + "requires": { + "@graphql-tools/merge": "8.3.16", + "@graphql-tools/utils": "9.1.4", + "tslib": "^2.4.0", + "value-or-promise": "1.0.12" + } + }, + "@graphql-tools/utils": { + "version": "9.1.4", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.1.4.tgz", + "integrity": "sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==", + "requires": { + "tslib": "^2.4.0" + } + }, + "@graphql-yoga/subscription": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@graphql-yoga/subscription/-/subscription-3.1.0.tgz", + "integrity": "sha512-Vc9lh8KzIHyS3n4jBlCbz7zCjcbtQnOBpsymcRvHhFr2cuH+knmRn0EmzimMQ58jQ8kxoRXXC3KJS3RIxSdPIg==", + "requires": { + "@graphql-yoga/typed-event-target": "^1.0.0", + "@repeaterjs/repeater": "^3.0.4", + "@whatwg-node/events": "0.0.2", + "tslib": "^2.3.1" + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "value-or-promise": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", + "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==" + } + } }, "har-schema": { "version": "2.0.0", @@ -18770,8 +19396,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz", "integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==", - "dev": true, - "requires": {} + "dev": true }, "isstream": { "version": "0.1.2", @@ -19163,8 +19788,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "27.0.6", @@ -19973,6 +20597,12 @@ "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", "dev": true }, + "lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, "lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -20110,7 +20740,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "requires": { "yallist": "^4.0.0" } @@ -20178,8 +20807,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.0.tgz", "integrity": "sha512-3QRZIS707pZQnijHdhbttXRWwrHhZJ/gzolneoxKVz9N/xmsvY/7Ls8lpnI9gxbgxjcHsAVEW3mgwiZCo6kkJQ==", - "dev": true, - "requires": {} + "dev": true }, "methods": { "version": "1.1.2", @@ -20947,7 +21575,7 @@ "version": "3.12.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-3.12.0.tgz", "integrity": "sha512-ltCMZAx1i0i9xuPM692Srj8McC665h6E5RqJom999sjtVSccHSD8Z+HSdBN2183h9PJKvC5dapkn78dd0NWMBg==", - "devOptional": true, + "dev": true, "requires": { "@prisma/engines": "3.12.0-37.22b822189f46ef0dc5c5b503368d1bee01213980" } @@ -21022,6 +21650,26 @@ "escape-goat": "^2.0.0" } }, + "pvtsutils": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.2.tgz", + "integrity": "sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==", + "requires": { + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, + "pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==" + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -21642,6 +22290,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -21919,7 +22572,8 @@ "tiny-lru": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-7.0.6.tgz", - "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==" + "integrity": "sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==", + "dev": true }, "title-case": { "version": "3.0.3", @@ -22157,9 +22811,12 @@ "dev": true }, "undici": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.0.0.tgz", - "integrity": "sha512-VhUpiZ3No1DOPPQVQnsDZyfcbTTcHdcgWej1PdFnSvOeJmOVDgiOHkunJmBLfmjt4CqgPQddPVjSWW0dsTs5Yg==" + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.16.0.tgz", + "integrity": "sha512-KWBOXNv6VX+oJQhchXieUznEmnJMqgXMbs0xxH2t8q/FUAWSJvOSr/rMaZKnX5RIVq7JDn0JbP4BOnKG2SGXLQ==", + "requires": { + "busboy": "^1.6.0" + } }, "unique-string": { "version": "2.0.0", @@ -22268,6 +22925,14 @@ "prepend-http": "^2.0.0" } }, + "urlpattern-polyfill": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-6.0.2.tgz", + "integrity": "sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg==", + "requires": { + "braces": "^3.0.2" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -22385,6 +23050,25 @@ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" }, + "webcrypto-core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.5.tgz", + "integrity": "sha512-gaExY2/3EHQlRNNNVSrbG2Cg94Rutl7fAaKILS1w8ZDhGxdFOaw6EbCfHIxPy9vt/xwp5o0VQAx9aySPF6hU1A==", + "requires": { + "@peculiar/asn1-schema": "^2.1.6", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.1", + "pvtsutils": "^1.3.2", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" + } + } + }, "webidl-conversions": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", @@ -22493,8 +23177,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", - "dev": true, - "requires": {} + "dev": true }, "xdg-basedir": { "version": "4.0.0", @@ -22523,8 +23206,7 @@ "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { "version": "1.10.2", @@ -22569,6 +23251,11 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + }, + "zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==" } } } diff --git a/server/package.json b/server/package.json index c178b66..508ee35 100644 --- a/server/package.json +++ b/server/package.json @@ -14,20 +14,22 @@ "db:seed": "prisma db seed" }, "prisma": { - "seed": "ts-node prisma/seed.ts" + "schema": "src/shared/infra/database/prisma/schema.prisma", + "seed": "ts-node src/shared/infra/database/prisma/seed.ts" }, "author": "", "license": "ISC", "dependencies": { "@graphql-tools/graphql-file-loader": "^7.3.10", "@graphql-tools/load": "^7.5.9", - "@graphql-yoga/node": "^2.3.0", "@prisma/client": "^3.12.0", "express": "^4.17.1", "graphql": "^16.3.0", + "graphql-yoga": "^3.4.0", "oso": "0.25.0", "reflect-metadata": "^0.1.13", - "sqlite3": "^5.0.2" + "sqlite3": "^5.0.2", + "zod": "^3.20.2" }, "devDependencies": { "@faker-js/faker": "^6.2.0", @@ -41,6 +43,7 @@ "@types/node": "^16.11.27", "@types/supertest": "^2.0.11", "@types/yargs": "^17.0.4", + "fishery": "^2.2.2", "jest": "^27.3.1", "nodemon": "^2.0.14", "prisma": "^3.12.0", diff --git a/server/schema.graphql b/server/schema.graphql index bd6753b..d7fe0e6 100644 --- a/server/schema.graphql +++ b/server/schema.graphql @@ -1,11 +1,11 @@ type Query { - userInfo: UserInfo! - listAllMembers: ListAllMembersResponse! - showMemberDetail(id: ID!): ShowMemberDetailResponse! + userInfo: UserInfo + listAllMembers: ListAllMembersResponse + showMemberDetail(id: ID!): ShowMemberDetailResponse } type Mutation { - editMemberDetail(input: EditMemberDetailInput!): EditMemberDetailResponse! + editMemberDetail(input: EditMemberDetailInput!): EditMemberDetailResponse } input EditMemberDetailInput { diff --git a/server/src/app.ts b/server/src/app.ts index 8a189f8..5f796ac 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -1,76 +1,3 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import express, { Application } from 'express'; -import { createUsersRouter } from '@/users/usersRouter'; -import { OsoDataFilter } from './auth/shared/repository/osoDataFilter'; -import { - createCoreOso, - createSqliteDataFilterOso, -} from './auth/shared/createOso'; +import { startServer } from './shared/infra/http/app'; -import 'reflect-metadata'; - -import { AuthorizeSqliteRepository } from './auth/shared/repository/authorizeSqliteRepository'; -import { createMembersRouter } from './members/membersRouter'; -import { PrismaClient } from '@prisma/client'; -import { loadSchema } from '@graphql-tools/load'; -import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; -import { createContext } from './context'; -import { createServer } from '@graphql-yoga/node'; -import { createCheckLoggedInMiddleware } from './auth/check-logged-in/checkLoggedInMiddleware'; -import { resolvers } from './resolvers'; - -export const prisma = new PrismaClient(); - -async function start() { - const oso = await createCoreOso(); - const authorizeRepository = new AuthorizeSqliteRepository(prisma); - const authorizer = new Authorizer(authorizeRepository, oso); - - const dataFilterOso = await createSqliteDataFilterOso(); - const dataFilter = new OsoDataFilter(dataFilterOso); - - // users - const usersRouter = createUsersRouter({ - dataFilter, - authorizer, - prisma, - }); - - // members - const membersRouter = createMembersRouter({ - dataFilter, - authorizer, - prisma, - }); - - // express - const app: Application = express(); - app.use(express.json()); - - app.use('/users', usersRouter); - app.use('/members', membersRouter); - - // // Build apollo-server-based graphql endpoint (trial) - const schema = await loadSchema('schema.graphql', { - loaders: [new GraphQLFileLoader()], - }); - const graphQLServer = createServer({ - schema: { - typeDefs: schema, - resolvers: resolvers, - }, - context: createContext({ dataFilter, authorizer, prisma }), - plugins: [], - }); - - app.use('/graphql', createCheckLoggedInMiddleware(authorizer)); - - app.use('/graphql', graphQLServer.requestListener); - - const port: number = 3031; - app.listen(port, function () { - console.log(`App is listening on port ${port} !`); - }); -} - -start(); +startServer(); diff --git a/server/src/auth/shared/authorizeRepository.ts b/server/src/auth/shared/authorizeRepository.ts deleted file mode 100644 index b8b8e6d..0000000 --- a/server/src/auth/shared/authorizeRepository.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { User } from '@/users/shared/user'; - -export interface AuthorizeRepository { - getUser(userId: string): Promise; -} diff --git a/server/src/auth/shared/authorizer.ts b/server/src/auth/shared/authorizer.ts deleted file mode 100644 index ae1216d..0000000 --- a/server/src/auth/shared/authorizer.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { AppError, ErrorCodes } from '@/shared/appError'; -import { User } from '@/users/shared/user'; -import { Oso } from 'oso'; -import { AuthorizeRepository } from './authorizeRepository'; - -export class Authorizer { - _currentUser?: User; - - constructor( - private readonly repository: AuthorizeRepository, - private readonly oso: Oso - ) {} - - get currentUser() { - if (!this._currentUser) { - throw new AppError('invalid operation', ErrorCodes.INVALID_OPERATION); - } - return this._currentUser; - } - - async setUser(userId: string): Promise { - const user = await this.repository.getUser(userId); - this._currentUser = user; - } - - isAllowed(actor: any, action: any, resource: any): Promise { - return this.oso.isAllowed(actor, action, resource); - } - - async authorizedFieldsForUser( - action: any, - resource: R - ): Promise> { - return this.authorizedFields(this.currentUser, action, resource); - } - - async authorizedActionsForUser(resource: R): Promise> { - return this.authorizedActions(this.currentUser, resource); - } - - private async authorizedFields(actor: any, action: any, resource: R) { - const set = await this.oso.authorizedFields(actor, action, resource); - return set as Set; - } - - private async authorizedActions(actor: any, resource: R) { - const set = await this.oso.authorizedActions(actor, resource); - return set as Set; - } -} diff --git a/server/src/auth/shared/dataFilter.ts b/server/src/auth/shared/dataFilter.ts deleted file mode 100644 index 88feb36..0000000 --- a/server/src/auth/shared/dataFilter.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface DataFilter { - authorizedResources( - actor: any, - action: any, - resource: any - ): Promise; - - authorizedQuery(actor: any, action: any, resource: any): Promise; -} diff --git a/server/src/auth/shared/repository/authorizeSqliteRepository.ts b/server/src/auth/shared/repository/authorizeSqliteRepository.ts deleted file mode 100644 index 051ea6c..0000000 --- a/server/src/auth/shared/repository/authorizeSqliteRepository.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; -import { AppError, ErrorCodes } from '@/shared/appError'; -import { User } from '@/users/shared/user'; -import { PrismaClient } from '@prisma/client'; -import { AuthorizeRepository } from '../authorizeRepository'; - -export class AuthorizeSqliteRepository implements AuthorizeRepository { - private readonly prisma: PrismaClient; - constructor(prisma: PrismaClient) { - this.prisma = prisma; - } - - async getUser(userId: string): Promise { - const userRecord = await this.prisma.user.findUnique({ - where: { - id: userId, - }, - include: { - member: { - include: { - department: true, - }, - }, - }, - }); - - if (!userRecord) { - throw new AppError( - `user id ${userId} not found`, - ErrorCodes.USER_NOT_FOUND - ); - } - - // TODO: implement helper method - const user = new User( - userRecord.id, - userRecord.username, - new Member( - userRecord.member.id, - userRecord.member.avatar, - userRecord.member.firstName, - userRecord.member.lastName, - userRecord.member.age, - userRecord.member.salary, - new Department( - userRecord.member.department.id, - userRecord.member.department.name, - userRecord.member.department.managerMemberId - ), - userRecord.member.joinedAt, - userRecord.member.phoneNumber, - userRecord.member.email, - userRecord.member.pr - ), - userRecord.isAdmin - ); - - return user; - } -} diff --git a/server/src/auth/shared/repository/osoDataFilter.ts b/server/src/auth/shared/repository/osoDataFilter.ts deleted file mode 100644 index d2c02e5..0000000 --- a/server/src/auth/shared/repository/osoDataFilter.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Oso } from 'oso'; -import { DataFilter } from '../dataFilter'; -import { NotAuthorizedError } from '../errors/not-authorized-error'; - -export class OsoDataFilter implements DataFilter { - constructor(private readonly oso: Oso) {} - - authorizedResources( - actor: any, - action: any, - resource: any - ): Promise { - return this.oso.authorizedResources(actor, action, resource); - } - - async authorizedQuery(actor: any, action: any, resource: any) { - const query = (await this.oso.authorizedQuery( - actor, - action, - resource - )) as any; - if (query === null) { - throw new NotAuthorizedError( - `no rules found: actor:${actor}, action:${action}, resource:${resource}` - ); - } - return query; - } -} diff --git a/server/src/context.ts b/server/src/context.ts deleted file mode 100644 index 5727611..0000000 --- a/server/src/context.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { PrismaClient } from '@prisma/client'; -import { Authorizer } from './auth/shared/authorizer'; -import { DataFilter } from './auth/shared/dataFilter'; -import { EditMemberDetailService } from './members/edit-member-detail/editMemberDetailService'; -import { EditMemberDetailSqliteRepository } from './members/edit-member-detail/repository/editMemberDetailSqliteRepository'; -import { ListAllMembersService } from './members/list-all-members/listAllMembersService'; -import { ListAllMembersSqliteRepository } from './members/list-all-members/repository/listAllMembersSqliteRepository'; -import { ShowMemberDetailSqliteRepository } from './members/show-member-detail/repository/showMemberDetailSqliteRepository'; -import { ShowMemberDetailService } from './members/show-member-detail/showMemberDetailService'; -import { GetLoggedInUserInfoService } from './users/get-logged-in-user-info/getLoggedInUserInfoService'; -import { GetLoggedInUserInfoSqliteRepository } from './users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository'; - -type Dependencies = { - dataFilter: DataFilter; - authorizer: Authorizer; - prisma: PrismaClient; -}; - -export const createContext = - ({ dataFilter, authorizer, prisma }: Dependencies) => - async ({ req }: any): Promise => { - const getLoggedInUserInfoRepository = - new GetLoggedInUserInfoSqliteRepository(dataFilter, prisma); - const getLoggedInUserInfoService = new GetLoggedInUserInfoService( - authorizer, - getLoggedInUserInfoRepository - ); - - const listAllMembersRepository = new ListAllMembersSqliteRepository( - dataFilter, - prisma - ); - const listAllMembersService = new ListAllMembersService( - authorizer, - listAllMembersRepository - ); - const showMemberDetailRepository = new ShowMemberDetailSqliteRepository( - dataFilter, - prisma - ); - const showMemberDetailService = new ShowMemberDetailService( - authorizer, - showMemberDetailRepository - ); - const editMemberDetailRepository = new EditMemberDetailSqliteRepository( - dataFilter, - prisma - ); - const editMemberDetailService = new EditMemberDetailService( - authorizer, - editMemberDetailRepository - ); - - return { - getLoggedInUserInfoService, - listAllMembersService, - showMemberDetailService, - editMemberDetailService, - }; - }; - -export interface Context { - getLoggedInUserInfoService: GetLoggedInUserInfoService; - listAllMembersService: ListAllMembersService; - showMemberDetailService: ShowMemberDetailService; - editMemberDetailService: EditMemberDetailService; -} diff --git a/server/src/members/edit-member-detail/editMemberDetailService.ts b/server/src/members/edit-member-detail/editMemberDetailService.ts deleted file mode 100644 index 02f680e..0000000 --- a/server/src/members/edit-member-detail/editMemberDetailService.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { NotAuthorizedError } from '@/auth/shared/errors/not-authorized-error'; -import { AppError, ErrorCodes } from '@/shared/appError'; -import { Member } from '../shared/member'; -import { - EditMemberDetailRepository, - UpdatePayload, -} from './editMemberDetailRepository'; - -export type EditMemberDetailRequest = { - memberId: string; - payload: UpdatePayload; -}; -export type EditMemberDetailResponse = { - result: boolean; -}; - -export class EditMemberDetailService { - constructor( - private readonly authorizer: Authorizer, - private readonly repository: EditMemberDetailRepository - ) {} - - async execute({ - memberId, - payload, - }: EditMemberDetailRequest): Promise { - try { - const member = await this.repository.queryMember(memberId); - - const authorizedFields = - await this.authorizer.authorizedFieldsForUser( - MEMBER_ACTIONS.UPDATE, - member - ); - - const authorizedPayload: UpdatePayload = { - ...(authorizedFields.has('firstName') && { - firstName: payload.firstName, - }), - ...(authorizedFields.has('lastName') && { lastName: payload.lastName }), - ...(authorizedFields.has('phoneNumber') && { - phoneNumber: payload.phoneNumber, - }), - ...(authorizedFields.has('email') && { email: payload.email }), - ...(authorizedFields.has('pr') && { pr: payload.pr }), - ...(authorizedFields.has('age') && { age: payload.age }), - ...(authorizedFields.has('salary') && { salary: payload.salary }), - }; - - if (Object.keys(authorizedPayload).length === 0) { - throw new AppError( - 'nothing is able to be updated', - ErrorCodes.BAD_REQUEST - ); - } - - await this.repository.updateMember( - this.authorizer.currentUser, - memberId, - authorizedPayload - ); - - return { - result: true, - }; - } catch (error) { - if (error instanceof NotAuthorizedError) { - return { result: false }; - } - throw error; - } - } -} diff --git a/server/src/members/edit-member-detail/repository/editMemberDetailSqliteRepository.ts b/server/src/members/edit-member-detail/repository/editMemberDetailSqliteRepository.ts deleted file mode 100644 index e6f110e..0000000 --- a/server/src/members/edit-member-detail/repository/editMemberDetailSqliteRepository.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { MemberOrm } from '@/auth/shared/createOso'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; -import { AppError, ErrorCodes } from '@/shared/appError'; -import { User } from '@/users/shared/user'; -import { PrismaClient } from '@prisma/client'; -import { - EditMemberDetailRepository, - UpdatePayload, -} from '../editMemberDetailRepository'; - -export class EditMemberDetailSqliteRepository - implements EditMemberDetailRepository -{ - private readonly prisma: PrismaClient; - constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { - this.prisma = prisma; - } - - async updateMember( - user: User, - memberId: string, - payload: UpdatePayload - ): Promise { - await this.dataFilter.authorizedQuery( - user, - MEMBER_ACTIONS.READ, - MemberOrm - ); - - await this.prisma.member.update({ where: { id: memberId }, data: payload }); - } - - async queryMember(memberId: string): Promise { - const record = await this.prisma.member.findUnique({ - where: { id: memberId }, - include: { - department: true, - }, - }); - - if (!record) { - throw new AppError( - `member not found ${memberId}`, - ErrorCodes.MEMBER_NOT_FOUND - ); - } - - // TODO: implement helper methods - return new Member( - record.id, - record.avatar, - record.firstName, - record.lastName, - record.age, - record.salary, - new Department( - record.department.id, - record.department.name, - record.department.managerMemberId - ), - record.joinedAt, - record.phoneNumber, - record.email, - record.pr - ); - } -} diff --git a/server/src/members/list-all-members/listAllMembersRepository.ts b/server/src/members/list-all-members/listAllMembersRepository.ts deleted file mode 100644 index 56637e4..0000000 --- a/server/src/members/list-all-members/listAllMembersRepository.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { User } from '@/users/shared/user'; -import { Member } from '../shared/member'; - -export interface ListAllMembersRepository { - queryMembers(user: User): Promise; -} diff --git a/server/src/members/list-all-members/listAllMembersService.ts b/server/src/members/list-all-members/listAllMembersService.ts deleted file mode 100644 index fd85327..0000000 --- a/server/src/members/list-all-members/listAllMembersService.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { NotAuthorizedError } from '@/auth/shared/errors/not-authorized-error'; -import { DisplayableMember, Member } from '../shared/member'; -import { ListAllMembersRepository } from './listAllMembersRepository'; - -export type ListAllMembersRequest = {}; -export type ListAllMembersResponse = { - members: DisplayableMember[]; -}; - -export class ListAllMembersService { - constructor( - private readonly authorizer: Authorizer, - private readonly repository: ListAllMembersRepository - ) {} - - async execute(): Promise { - try { - const members = await this.repository.queryMembers( - this.authorizer.currentUser - ); - const authorizedMembers: DisplayableMember[] = []; - for (const member of members) { - const authorizedFields = - await this.authorizer.authorizedFieldsForUser( - MEMBER_ACTIONS.READ, - member - ); - - const authorizedMember = - member.createObjectWithAuthorizedFields(authorizedFields); - - const allowedActions = await this.authorizer.authorizedActionsForUser( - member - ); - if (allowedActions.has(MEMBER_ACTIONS.UPDATE)) { - authorizedMember.editable = true; - } - - if (member.id === this.authorizer.currentUser.memberInfo.id) { - authorizedMember.isLoggedInUser = true; - } - - authorizedMembers.push(authorizedMember); - } - - return { - members: authorizedMembers, - }; - } catch (error) { - if (error instanceof NotAuthorizedError) { - return { members: [] }; - } - throw error; - } - } -} diff --git a/server/src/members/list-all-members/repository/listAllMembersSqliteRepository.ts b/server/src/members/list-all-members/repository/listAllMembersSqliteRepository.ts deleted file mode 100644 index 15e520b..0000000 --- a/server/src/members/list-all-members/repository/listAllMembersSqliteRepository.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { MemberOrm } from '@/auth/shared/createOso'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; -import { User } from '@/users/shared/user'; -import { PrismaClient } from '@prisma/client'; -import { ListAllMembersRepository } from '../listAllMembersRepository'; - -export class ListAllMembersSqliteRepository - implements ListAllMembersRepository -{ - private readonly prisma: PrismaClient; - constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { - this.prisma = prisma; - } - - async queryMembers(user: User): Promise { - const query = await this.dataFilter.authorizedQuery( - user, - MEMBER_ACTIONS.READ, - MemberOrm - ); - - const records = await this.prisma.member.findMany({ - where: query, - include: { - department: true, - }, - }); - - return records.map((member) => { - return new Member( - member.id, - member.avatar, - member.firstName, - member.lastName, - member.age, - member.salary, - new Department( - member.department.id, - member.department.name, - member.department.managerMemberId - ), - member.joinedAt, - member.phoneNumber, - member.email, - member.pr - ); - }); - } -} diff --git a/server/src/members/membersController.ts b/server/src/members/membersController.ts deleted file mode 100644 index c38cb02..0000000 --- a/server/src/members/membersController.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Request, Response } from 'express'; -import { UpdatePayload } from './edit-member-detail/editMemberDetailRepository'; -import { EditMemberDetailService } from './edit-member-detail/editMemberDetailService'; -import { ListAllMembersService } from './list-all-members/listAllMembersService'; -import { ShowMemberDetailService } from './show-member-detail/showMemberDetailService'; - -type Dependencies = { - listAllMembersService: ListAllMembersService; - showMemberDetailService: ShowMemberDetailService; - editMemberDetailService: EditMemberDetailService; -}; - -export class MembersController { - private readonly listAllMembersService: ListAllMembersService; - private readonly showMemberDetailService: ShowMemberDetailService; - private readonly editMemberDetailService: EditMemberDetailService; - - constructor(deps: Dependencies) { - this.listAllMembersService = deps.listAllMembersService; - this.showMemberDetailService = deps.showMemberDetailService; - this.editMemberDetailService = deps.editMemberDetailService; - } - - listAllMembers = async (req: Request, res: Response) => { - const result = await this.listAllMembersService.execute(); - res.json(result); - }; - - showMemberDetail = async ( - req: Request<{ memberId: string }>, - res: Response - ) => { - const result = await this.showMemberDetailService.execute({ - memberId: req.params.memberId, - }); - res.json(result); - }; - - editMemberDetail = async ( - req: Request<{ memberId: string }, any, UpdatePayload>, - res: Response - ) => { - const { memberId } = req.params; - const result = await this.editMemberDetailService.execute({ - memberId, - payload: req.body, - }); - res.json(result); - }; -} diff --git a/server/src/members/membersRouter.ts b/server/src/members/membersRouter.ts deleted file mode 100644 index 1d196e3..0000000 --- a/server/src/members/membersRouter.ts +++ /dev/null @@ -1,65 +0,0 @@ -import express from 'express'; -import { Authorizer } from '@/auth/shared/authorizer'; -import { createCheckLoggedInMiddleware } from '@/auth/check-logged-in/checkLoggedInMiddleware'; -import { MembersController } from './membersController'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { ListAllMembersSqliteRepository } from './list-all-members/repository/listAllMembersSqliteRepository'; -import { ListAllMembersService } from './list-all-members/listAllMembersService'; -import { ShowMemberDetailSqliteRepository } from './show-member-detail/repository/showMemberDetailSqliteRepository'; -import { ShowMemberDetailService } from './show-member-detail/showMemberDetailService'; -import { EditMemberDetailSqliteRepository } from './edit-member-detail/repository/editMemberDetailSqliteRepository'; -import { EditMemberDetailService } from './edit-member-detail/editMemberDetailService'; -import { asyncHandler } from '@/shared/asyncHandler'; -import { PrismaClient } from '@prisma/client'; - -type Dependencies = { - dataFilter: DataFilter; - authorizer: Authorizer; - prisma: PrismaClient; -}; - -export function createMembersRouter({ - dataFilter, - authorizer, - prisma, -}: Dependencies) { - const listAllMembersRepository = new ListAllMembersSqliteRepository( - dataFilter, - prisma - ); - const listAllMembersService = new ListAllMembersService( - authorizer, - listAllMembersRepository - ); - const showMemberDetailRepository = new ShowMemberDetailSqliteRepository( - dataFilter, - prisma - ); - const showMemberDetailService = new ShowMemberDetailService( - authorizer, - showMemberDetailRepository - ); - const editMemberDetailRepository = new EditMemberDetailSqliteRepository( - dataFilter, - prisma - ); - const editMemberDetailService = new EditMemberDetailService( - authorizer, - editMemberDetailRepository - ); - const membersController = new MembersController({ - listAllMembersService, - showMemberDetailService, - editMemberDetailService, - }); - - const router = express.Router(); - - router.use(createCheckLoggedInMiddleware(authorizer)); - - router.get('/', asyncHandler(membersController.listAllMembers)); - router.get('/:memberId', asyncHandler(membersController.showMemberDetail)); - router.patch('/:memberId', asyncHandler(membersController.editMemberDetail)); - - return router; -} diff --git a/server/src/members/shared/member.ts b/server/src/members/shared/member.ts deleted file mode 100644 index 5b410c1..0000000 --- a/server/src/members/shared/member.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NonFunctionPropertyNames } from '@/shared/sharedTypes'; -import { Department } from './department'; - -export class Member { - static PUBLIC_FIELDS: NonFunctionPropertyNames[] = [ - 'id', - 'avatar', - 'firstName', - 'lastName', - 'department', - 'joinedAt', - 'phoneNumber', - 'email', - 'pr', - ]; - static PRIVATE_FIELDS: NonFunctionPropertyNames[] = ['age', 'salary']; - - constructor( - public id: string, - public avatar: string, - public firstName: string, - public lastName: string, - public age: number, - public salary: number, - public department: Department, - public joinedAt: Date, - public phoneNumber: string, - public email: string, - public pr: string - ) {} - - createObjectWithAuthorizedFields( - this: any, - fields: Set - ): DisplayableMember { - const authorizedMember: DisplayableMember = {}; - - for (const field of Array.from(fields.values())) { - authorizedMember[field as keyof DisplayableMember] = this[field]; - } - - return authorizedMember; - } -} - -export type DisplayableMember = { - id?: string; - avatar?: string; - firstName?: string; - lastName?: string; - age?: number; - salary?: number; - department?: Department; - joinedAt?: Date; - phoneNumber?: string; - email?: string; - pr?: string; - editable?: boolean; - isLoggedInUser?: boolean; -}; diff --git a/server/src/members/show-member-detail/repository/showMemberDetailSqliteRepository.ts b/server/src/members/show-member-detail/repository/showMemberDetailSqliteRepository.ts deleted file mode 100644 index 590cd42..0000000 --- a/server/src/members/show-member-detail/repository/showMemberDetailSqliteRepository.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { MemberOrm } from '@/auth/shared/createOso'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; -import { AppError, ErrorCodes } from '@/shared/appError'; -import { User } from '@/users/shared/user'; -import { PrismaClient } from '@prisma/client'; -import { ShowMemberDetailRepository } from '../showMemberDetailRepository'; - -export class ShowMemberDetailSqliteRepository - implements ShowMemberDetailRepository -{ - private readonly prisma: PrismaClient; - constructor(private readonly dataFilter: DataFilter, prisma: PrismaClient) { - this.prisma = prisma; - } - - async queryMember(user: User, memberId: string): Promise { - await this.dataFilter.authorizedQuery( - user, - MEMBER_ACTIONS.READ, - MemberOrm - ); - - const record = await this.prisma.member.findUnique({ - where: { id: memberId }, - include: { department: true }, - }); - - if (!record) { - throw new AppError( - `member not found ${memberId}`, - ErrorCodes.MEMBER_NOT_FOUND - ); - } - - return new Member( - record.id, - record.avatar, - record.firstName, - record.lastName, - record.age, - record.salary, - new Department( - record.department.id, - record.department.name, - record.department.managerMemberId - ), - record.joinedAt, - record.phoneNumber, - record.email, - record.pr - ); - } -} diff --git a/server/src/members/show-member-detail/showMemberDetailRepository.ts b/server/src/members/show-member-detail/showMemberDetailRepository.ts deleted file mode 100644 index 37704a5..0000000 --- a/server/src/members/show-member-detail/showMemberDetailRepository.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { User } from '@/users/shared/user'; -import { Member } from '../shared/member'; - -export interface ShowMemberDetailRepository { - queryMember(user: User, memberId: string): Promise; -} diff --git a/server/src/members/show-member-detail/showMemberDetailService.ts b/server/src/members/show-member-detail/showMemberDetailService.ts deleted file mode 100644 index 4013765..0000000 --- a/server/src/members/show-member-detail/showMemberDetailService.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import { MEMBER_ACTIONS } from '@/auth/shared/constants/actions'; -import { NotAuthorizedError } from '@/auth/shared/errors/not-authorized-error'; -import { DisplayableMember, Member } from '../shared/member'; -import { ShowMemberDetailRepository } from './showMemberDetailRepository'; - -export type ShowMemberDetailRequest = { - memberId: string; -}; -export type ShowMemberDetailResponse = { - editableFields: string[]; - member: DisplayableMember; -}; - -export class ShowMemberDetailService { - constructor( - private readonly authorizer: Authorizer, - private readonly repository: ShowMemberDetailRepository - ) {} - - async execute( - req: ShowMemberDetailRequest - ): Promise { - const member = await this.repository.queryMember( - this.authorizer.currentUser, - req.memberId - ); - - try { - const authorizedFields = - await this.authorizer.authorizedFieldsForUser( - MEMBER_ACTIONS.READ, - member - ); - const authorizedMember = - member.createObjectWithAuthorizedFields(authorizedFields); - - const allowedActions = await this.authorizer.authorizedActionsForUser( - member - ); - - let authorizedFieldsToUpdate: string[] = []; - if (allowedActions.has(MEMBER_ACTIONS.UPDATE)) { - authorizedMember.editable = true; - - const fields = await this.authorizer.authorizedFieldsForUser( - MEMBER_ACTIONS.UPDATE, - member - ); - // FIXME: exclude readonly fields such as id, joinedAt - authorizedFieldsToUpdate = Array.from(fields.values()).map((f) => f); - } - - if (member.id === this.authorizer.currentUser.memberInfo.id) { - authorizedMember.isLoggedInUser = true; - } - - return { - editableFields: authorizedFieldsToUpdate, - member: authorizedMember, - }; - } catch (error) { - // TODO: return 404 NOT FOUND - throw error; - } - } -} diff --git a/server/src/auth/policies/main.polar b/server/src/modules/auth/policies/main.polar similarity index 100% rename from server/src/auth/policies/main.polar rename to server/src/modules/auth/policies/main.polar diff --git a/server/src/auth/policies/members.polar b/server/src/modules/auth/policies/members.polar similarity index 100% rename from server/src/auth/policies/members.polar rename to server/src/modules/auth/policies/members.polar diff --git a/server/src/auth/policies/users.polar b/server/src/modules/auth/policies/users.polar similarity index 100% rename from server/src/auth/policies/users.polar rename to server/src/modules/auth/policies/users.polar diff --git a/server/src/modules/auth/shared/authorizer.ts b/server/src/modules/auth/shared/authorizer.ts new file mode 100644 index 0000000..977200e --- /dev/null +++ b/server/src/modules/auth/shared/authorizer.ts @@ -0,0 +1,101 @@ +import { UserDTO } from '@/modules/users/dtos/userDTO'; +import { InvalidOperationError } from '@/shared/core/errors/invalidOperationError'; +import { Result } from '@/shared/core/result'; +import { PrismaClient } from '@prisma/client'; +import { Oso } from 'oso'; +import { NotAuthorizedError } from './errors/notAuthorizedError'; +import { UserNotFoundError } from './errors/userNotFoundError'; + +export class Authorizer { + _currentUser?: UserDTO; + + constructor( + private readonly prisma: PrismaClient, + private readonly coreOso: Oso, + private readonly filterOso: Oso + ) {} + + get currentUser() { + if (!this._currentUser) { + throw new InvalidOperationError(); + } + return this._currentUser; + } + + async setUser(userId: string): Promise { + const userRecord = await this.prisma.user.findUnique({ + where: { + id: userId, + }, + include: { + member: { + include: { + department: true, + }, + }, + }, + }); + + if (!userRecord) { + throw new UserNotFoundError(userId); + } + + const user = UserDTO.createFromOrmModel(userRecord); + + this._currentUser = user; + } + + isAllowed(actor: any, action: any, resource: any): Promise { + return this.coreOso.isAllowed(actor, action, resource); + } + + async authorizedFieldsForUser( + action: any, + resource: R + ): Promise>> { + try { + const fields = await this.authorizedFields( + this.currentUser, + action, + resource + ); + + return Result.ok(fields); + } catch (err: any) { + return Result.fail(err); + } + } + + async authorizedActionsForUser(resource: R): Promise>> { + try { + const actions = await this.authorizedActions(this.currentUser, resource); + return Result.ok(actions); + } catch (err: any) { + return Result.fail(err); + } + } + + async authorizedQueryForUser(action: any, resource: any) { + const query = (await this.filterOso.authorizedQuery( + this.currentUser, + action, + resource + )) as any; + if (query === null) { + throw new NotAuthorizedError( + `no rules found: actor:${this.currentUser}, action:${action}, resource:${resource}` + ); + } + return query; + } + + private async authorizedFields(actor: any, action: any, resource: R) { + const set = await this.coreOso.authorizedFields(actor, action, resource); + return set as Set; + } + + private async authorizedActions(actor: any, resource: R) { + const set = await this.coreOso.authorizedActions(actor, resource); + return set as Set; + } +} diff --git a/server/src/auth/check-logged-in/checkLoggedInMiddleware.ts b/server/src/modules/auth/shared/checkLoggedInMiddleware.ts similarity index 90% rename from server/src/auth/check-logged-in/checkLoggedInMiddleware.ts rename to server/src/modules/auth/shared/checkLoggedInMiddleware.ts index d5af6a6..563270a 100644 --- a/server/src/auth/check-logged-in/checkLoggedInMiddleware.ts +++ b/server/src/modules/auth/shared/checkLoggedInMiddleware.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express'; -import { Authorizer } from '@/auth/shared/authorizer'; +import { Authorizer } from './authorizer'; export function createCheckLoggedInMiddleware(authorizer: Authorizer) { return async function checkLoggedInMiddleware( diff --git a/server/src/auth/shared/constants/actions.ts b/server/src/modules/auth/shared/constants/actions.ts similarity index 100% rename from server/src/auth/shared/constants/actions.ts rename to server/src/modules/auth/shared/constants/actions.ts diff --git a/server/src/auth/shared/createOso.ts b/server/src/modules/auth/shared/createOso.ts similarity index 87% rename from server/src/auth/shared/createOso.ts rename to server/src/modules/auth/shared/createOso.ts index e2c962a..a74c7e4 100644 --- a/server/src/auth/shared/createOso.ts +++ b/server/src/modules/auth/shared/createOso.ts @@ -1,23 +1,23 @@ -import { prisma } from '@/app'; -import { Department } from '@/members/shared/department'; -import { Member } from '@/members/shared/member'; import { Member as PrismaMember, Department as PrismaDepartment, UserMenuItem as PrismaUserMenuItem, } from '@prisma/client'; -import { User } from '@/users/shared/user'; -import { UserMenuItem } from '@/users/shared/userMenuItem'; import { Oso } from 'oso'; import { Filter, Relation } from 'oso/dist/src/dataFiltering'; import { PrimitivePropertyNames } from '@/shared/sharedTypes'; +import { prisma } from '@/shared/infra/http/app'; +import { DepartmentDTO } from '@/modules/members/dtos/departmentDTO'; +import { MemberDTO } from '@/modules/members/dtos/memberDTO'; +import { UserMenuItem } from '@/modules/users/dtos/userMenuItemDTO'; +import { UserDTO } from '@/modules/users/dtos/userDTO'; // FIXME: Since prisma objects are POJOs, we need to create classes // to pass to Oso by ourselves. // https://github.com/prisma/prisma/issues/5315 export class DepartmentOrm { static entityFieldMap: Record< - PrimitivePropertyNames, + PrimitivePropertyNames, PrimitivePropertyNames > = { id: 'id', @@ -31,7 +31,7 @@ export class DepartmentOrm { } export class MemberOrm { static entityFieldMap: Record< - PrimitivePropertyNames, + PrimitivePropertyNames, PrimitivePropertyNames > = { id: 'id', @@ -122,7 +122,7 @@ export async function createSqliteDataFilterOso() { }); // Since User will always be the LoggedInUser, we use the core entity class - osoDataFilter.registerClass(User, { + osoDataFilter.registerClass(UserDTO, { name: 'User', }); osoDataFilter.registerClass(UserMenuItemOrm, { @@ -165,10 +165,16 @@ export async function createSqliteDataFilterOso() { export async function createCoreOso() { const oso = new Oso(); - oso.registerClass(User); + oso.registerClass(UserDTO, { + name: 'User', + }); oso.registerClass(UserMenuItem); - oso.registerClass(Department); - oso.registerClass(Member); + oso.registerClass(DepartmentDTO, { + name: 'Department', + }); + oso.registerClass(MemberDTO, { + name: 'Member', + }); await oso.loadFiles(policyFiles); return oso; diff --git a/server/src/auth/shared/errors/not-authorized-error.ts b/server/src/modules/auth/shared/errors/notAuthorizedError.ts similarity index 100% rename from server/src/auth/shared/errors/not-authorized-error.ts rename to server/src/modules/auth/shared/errors/notAuthorizedError.ts diff --git a/server/src/modules/auth/shared/errors/userNotFoundError.ts b/server/src/modules/auth/shared/errors/userNotFoundError.ts new file mode 100644 index 0000000..d14e104 --- /dev/null +++ b/server/src/modules/auth/shared/errors/userNotFoundError.ts @@ -0,0 +1,7 @@ +import { AppError, ErrorCodes } from '@/shared/core/errors/appError'; + +export class UserNotFoundError extends AppError { + constructor(userId: string) { + super(`user not found: ${userId}`, ErrorCodes.USER_NOT_FOUND); + } +} diff --git a/server/src/members/shared/department.ts b/server/src/modules/members/dtos/departmentDTO.ts similarity index 78% rename from server/src/members/shared/department.ts rename to server/src/modules/members/dtos/departmentDTO.ts index be5a462..c868d81 100644 --- a/server/src/members/shared/department.ts +++ b/server/src/modules/members/dtos/departmentDTO.ts @@ -1,4 +1,4 @@ -export class Department { +export class DepartmentDTO { constructor( public id: string, public name: string, diff --git a/server/src/modules/members/dtos/displayableMemberDTO.ts b/server/src/modules/members/dtos/displayableMemberDTO.ts new file mode 100644 index 0000000..5a0298f --- /dev/null +++ b/server/src/modules/members/dtos/displayableMemberDTO.ts @@ -0,0 +1,6 @@ +import { MemberDTO } from './memberDTO'; + +export type DisplayableMember = Partial & { + editable: boolean; + isLoggedInUser: boolean; +}; diff --git a/server/src/modules/members/dtos/memberDTO.ts b/server/src/modules/members/dtos/memberDTO.ts new file mode 100644 index 0000000..9311b55 --- /dev/null +++ b/server/src/modules/members/dtos/memberDTO.ts @@ -0,0 +1,70 @@ +import { NonFunctionPropertyNames } from '@/shared/sharedTypes'; +import { DepartmentDTO } from './departmentDTO'; +import { + Member as PrismaMember, + Department as PrismaDepartment, +} from '@prisma/client'; + +export class MemberDTO { + static PUBLIC_FIELDS: NonFunctionPropertyNames[] = [ + 'id', + 'avatar', + 'firstName', + 'lastName', + 'department', + 'joinedAt', + 'phoneNumber', + 'email', + 'pr', + ]; + static PRIVATE_FIELDS: NonFunctionPropertyNames[] = ['age', 'salary']; + + constructor( + public id: string, + public avatar: string, + public firstName: string, + public lastName: string, + public age: number, + public salary: number, + public department: DepartmentDTO, + public joinedAt: Date, + public phoneNumber: string, + public email: string, + public pr: string + ) {} + + createObjectWithAuthorizedFields( + this: any, + fields: Set + ): Partial { + const authorizedMember: Partial = {}; + + for (const field of Array.from(fields.values())) { + authorizedMember[field as keyof Partial] = this[field]; + } + + return authorizedMember; + } + + static createFromOrmModel( + member: PrismaMember & { department: PrismaDepartment } + ): MemberDTO { + return new MemberDTO( + member.id, + member.avatar, + member.firstName, + member.lastName, + member.age, + member.salary, + new DepartmentDTO( + member.department.id, + member.department.name, + member.department.managerMemberId + ), + member.joinedAt, + member.phoneNumber, + member.email, + member.pr + ); + } +} diff --git a/server/src/modules/members/infra/repos/prismaMemberRepository.ts b/server/src/modules/members/infra/repos/prismaMemberRepository.ts new file mode 100644 index 0000000..2d52c06 --- /dev/null +++ b/server/src/modules/members/infra/repos/prismaMemberRepository.ts @@ -0,0 +1,68 @@ +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { MemberOrm } from '@/modules/auth/shared/createOso'; +import { Result } from '@/shared/core/result'; +import { PrismaClient } from '@prisma/client'; +import { DepartmentDTO } from '../../dtos/departmentDTO'; +import { MemberDTO } from '../../dtos/memberDTO'; +import { + EditMemberDetailRepository, + UpdatePayload, +} from '../../useCases/command/editMemberDetail/editMemberDetailRepository'; +import { MemberNotFoundError } from '../../useCases/errors/memberNotFoundError'; + +export class PrismaMemberRepository implements EditMemberDetailRepository { + private readonly prisma: PrismaClient; + constructor(private readonly authorizer: Authorizer, prisma: PrismaClient) { + this.prisma = prisma; + } + + async updateMember( + memberId: string, + payload: UpdatePayload + ): Promise> { + await this.authorizer.authorizedQueryForUser( + MEMBER_ACTIONS.READ, + MemberOrm + ); + + await this.prisma.member.update({ where: { id: memberId }, data: payload }); + + return Result.ok(); + } + + async queryMember(memberId: string): Promise> { + await this.authorizer.authorizedQueryForUser( + MEMBER_ACTIONS.READ, + MemberOrm + ); + + const record = await this.prisma.member.findUnique({ + where: { id: memberId }, + include: { department: true }, + }); + + if (!record) { + throw new MemberNotFoundError(memberId); + } + + const member = new MemberDTO( + record.id, + record.avatar, + record.firstName, + record.lastName, + record.age, + record.salary, + new DepartmentDTO( + record.department.id, + record.department.name, + record.department.managerMemberId + ), + record.joinedAt, + record.phoneNumber, + record.email, + record.pr + ); + return Result.ok(member); + } +} diff --git a/server/src/members/edit-member-detail/editMemberDetailRepository.ts b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts similarity index 50% rename from server/src/members/edit-member-detail/editMemberDetailRepository.ts rename to server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts index c00b34a..c5d1a14 100644 --- a/server/src/members/edit-member-detail/editMemberDetailRepository.ts +++ b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailRepository.ts @@ -1,5 +1,5 @@ -import { User } from '@/users/shared/user'; -import { Member } from '../shared/member'; +import { Result } from '@/shared/core/result'; +import { MemberDTO } from '../../../dtos/memberDTO'; export type UpdatePayload = { firstName?: string; @@ -13,10 +13,6 @@ export type UpdatePayload = { }; export interface EditMemberDetailRepository { - queryMember(memberId: string): Promise; - updateMember( - user: User, - memberId: string, - payload: UpdatePayload - ): Promise; + queryMember(memberId: string): Promise>; + updateMember(memberId: string, payload: UpdatePayload): Promise>; } diff --git a/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts new file mode 100644 index 0000000..471524a --- /dev/null +++ b/server/src/modules/members/useCases/command/editMemberDetail/editMemberDetailService.ts @@ -0,0 +1,84 @@ +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { Result } from '@/shared/core/result'; +import { UseCase } from '@/shared/core/useCase'; +import { MemberDTO } from '../../../dtos/memberDTO'; +import { MemberNothingToUpdateError } from '../../errors/memberNothingToUpdateError'; +import { + EditMemberDetailRepository, + UpdatePayload, +} from './editMemberDetailRepository'; +import z from 'zod'; + +export const editMemberDetailPayloadSchema = z.object({ + firstName: z.string().optional(), + lastName: z.string().optional(), + age: z.number().optional(), + salary: z.number().optional(), + departmentId: z.string().uuid().optional(), + phoneNumber: z.string().optional(), + email: z.string().email().optional(), + pr: z.string().optional(), +}); + +export type EditMemberDetailRequest = { + memberId: string; + payload: z.infer; +}; +export type EditMemberDetailResponse = { + result: boolean; +}; + +export class EditMemberDetailService + implements UseCase> +{ + constructor( + private readonly authorizer: Authorizer, + private readonly repository: EditMemberDetailRepository + ) {} + + async execute({ + memberId, + payload, + }: EditMemberDetailRequest): Promise> { + const memberOrError = await this.repository.queryMember(memberId); + if (memberOrError.isFailure) { + return Result.fail(memberOrError.error); + } + + const authorizedFieldsOrError = + await this.authorizer.authorizedFieldsForUser( + MEMBER_ACTIONS.UPDATE, + memberOrError.getValue() + ); + if (authorizedFieldsOrError.isFailure) { + return Result.fail(authorizedFieldsOrError.error); + } + + const authorizedFields = authorizedFieldsOrError.getValue(); + + const authorizedPayload: UpdatePayload = { + ...(authorizedFields.has('firstName') && { + firstName: payload.firstName, + }), + ...(authorizedFields.has('lastName') && { lastName: payload.lastName }), + ...(authorizedFields.has('phoneNumber') && { + phoneNumber: payload.phoneNumber, + }), + ...(authorizedFields.has('email') && { email: payload.email }), + ...(authorizedFields.has('pr') && { pr: payload.pr }), + ...(authorizedFields.has('age') && { age: payload.age }), + ...(authorizedFields.has('salary') && { salary: payload.salary }), + }; + + if (Object.keys(authorizedPayload).length === 0) { + throw new MemberNothingToUpdateError(memberId); + } + + await this.repository.updateMember(memberId, authorizedPayload); + + return Result.ok({ + result: true, + }); + } +} diff --git a/server/src/modules/members/useCases/errors/memberNotFoundError.ts b/server/src/modules/members/useCases/errors/memberNotFoundError.ts new file mode 100644 index 0000000..63520f7 --- /dev/null +++ b/server/src/modules/members/useCases/errors/memberNotFoundError.ts @@ -0,0 +1,7 @@ +import { AppError, ErrorCodes } from '@/shared/core/errors/appError'; + +export class MemberNotFoundError extends AppError { + constructor(memberId: string) { + super(`member not found: ${memberId}`, ErrorCodes.MEMBER_NOT_FOUND); + } +} diff --git a/server/src/modules/members/useCases/errors/memberNothingToUpdateError.ts b/server/src/modules/members/useCases/errors/memberNothingToUpdateError.ts new file mode 100644 index 0000000..ff4ca65 --- /dev/null +++ b/server/src/modules/members/useCases/errors/memberNothingToUpdateError.ts @@ -0,0 +1,7 @@ +import { AppError, ErrorCodes } from '@/shared/core/errors/appError'; + +export class MemberNothingToUpdateError extends AppError { + constructor(memberId: string) { + super(`nothing is able to be updated: ${memberId}`, ErrorCodes.BAD_REQUEST); + } +} diff --git a/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts b/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts new file mode 100644 index 0000000..1fc0a76 --- /dev/null +++ b/server/src/modules/members/useCases/query/listAllMembers/listAllMembersService.ts @@ -0,0 +1,69 @@ +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { Result } from '@/shared/core/result'; +import { UseCase } from '@/shared/core/useCase'; +import { MemberDTO } from '../../../dtos/memberDTO'; +import { DisplayableMember } from '../../../dtos/displayableMemberDTO'; +import { MemberOrm } from '@/modules/auth/shared/createOso'; +import { PrismaClient } from '@prisma/client'; + +export type ListAllMembersRequest = {}; +export type ListAllMembersResponse = { + members: DisplayableMember[]; +}; + +export class ListAllMembersService + implements UseCase> +{ + constructor( + private readonly authorizer: Authorizer, + private readonly prisma: PrismaClient + ) {} + + async execute(): Promise> { + const query = await this.authorizer.authorizedQueryForUser( + MEMBER_ACTIONS.READ, + MemberOrm + ); + const memberModels = await this.prisma.member.findMany({ + where: query, + include: { + department: true, + }, + }); + + const displayableMembers: DisplayableMember[] = []; + for (const memberModel of memberModels) { + const memberDto = MemberDTO.createFromOrmModel(memberModel); + const authorizedFieldsOrError = + await this.authorizer.authorizedFieldsForUser( + MEMBER_ACTIONS.READ, + memberDto + ); + if (authorizedFieldsOrError.isFailure) { + return Result.fail(authorizedFieldsOrError.error); + } + + const allowedActionsOrError = + await this.authorizer.authorizedActionsForUser(memberDto); + if (allowedActionsOrError.isFailure) { + return Result.fail(allowedActionsOrError.error); + } + + const displayableMember: DisplayableMember = { + ...memberDto.createObjectWithAuthorizedFields( + authorizedFieldsOrError.getValue() + ), + editable: allowedActionsOrError.getValue().has(MEMBER_ACTIONS.UPDATE), + isLoggedInUser: + memberDto.id === this.authorizer.currentUser.memberInfo.id, + }; + + displayableMembers.push(displayableMember); + } + + return Result.ok({ + members: displayableMembers, + }); + } +} diff --git a/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts new file mode 100644 index 0000000..a70b06d --- /dev/null +++ b/server/src/modules/members/useCases/query/showMemberDetail/showMemberDetailService.ts @@ -0,0 +1,85 @@ +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { MEMBER_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { Result } from '@/shared/core/result'; +import { UseCase } from '@/shared/core/useCase'; +import { MemberDTO } from '../../../dtos/memberDTO'; +import { DisplayableMember } from '../../../dtos/displayableMemberDTO'; +import { PrismaClient } from '@prisma/client'; +import { MemberOrm } from '@/modules/auth/shared/createOso'; +import { MemberNotFoundError } from '../../errors/memberNotFoundError'; + +export type ShowMemberDetailRequest = { + memberId: string; +}; +export type ShowMemberDetailResponse = { + editableFields: string[]; + member: DisplayableMember; +}; + +export class ShowMemberDetailService + implements UseCase> +{ + constructor( + private readonly authorizer: Authorizer, + private readonly prisma: PrismaClient + ) {} + + async execute( + req: ShowMemberDetailRequest + ): Promise> { + await this.authorizer.authorizedQueryForUser( + MEMBER_ACTIONS.READ, + MemberOrm + ); + + const record = await this.prisma.member.findUnique({ + where: { id: req.memberId }, + include: { department: true }, + }); + + if (!record) { + return Result.fail(new MemberNotFoundError(req.memberId)); + } + + const memberDto = MemberDTO.createFromOrmModel(record); + const authorizedFieldsOrError = + await this.authorizer.authorizedFieldsForUser( + MEMBER_ACTIONS.READ, + memberDto + ); + + const allowedActionsOrError = + await this.authorizer.authorizedActionsForUser(memberDto); + if (allowedActionsOrError.isFailure) { + return Result.fail(allowedActionsOrError.error); + } + + const displayableMember: DisplayableMember = { + ...memberDto.createObjectWithAuthorizedFields( + authorizedFieldsOrError.getValue() + ), + editable: allowedActionsOrError.getValue().has(MEMBER_ACTIONS.UPDATE), + isLoggedInUser: + memberDto.id === this.authorizer.currentUser.memberInfo.id, + }; + + const editableFieldsOrError = + await this.authorizer.authorizedFieldsForUser( + MEMBER_ACTIONS.UPDATE, + memberDto + ); + if (editableFieldsOrError.isFailure) { + return Result.fail(editableFieldsOrError.error); + } + + // FIXME: exclude readonly fields such as id, joinedAt + const editableFields: string[] = Array.from( + editableFieldsOrError.getValue().values() + ); + + return Result.ok({ + editableFields: editableFields, + member: displayableMember, + }); + } +} diff --git a/server/src/modules/users/dtos/userDTO.ts b/server/src/modules/users/dtos/userDTO.ts new file mode 100644 index 0000000..60da892 --- /dev/null +++ b/server/src/modules/users/dtos/userDTO.ts @@ -0,0 +1,45 @@ +import { DepartmentDTO } from '@/modules/members/dtos/departmentDTO'; +import { MemberDTO } from '@/modules/members/dtos/memberDTO'; +import { + User as PrismaUser, + Member as PrismaMember, + Department as PrismaDepartment, +} from '@prisma/client'; + +export class UserDTO { + constructor( + public id: string, + public username: string, + public memberInfo: MemberDTO, + public isAdmin: boolean + ) {} + + static createFromOrmModel( + userRecord: PrismaUser & { + member: PrismaMember & { department: PrismaDepartment }; + } + ): UserDTO { + return new UserDTO( + userRecord.id, + userRecord.username, + new MemberDTO( + userRecord.member.id, + userRecord.member.avatar, + userRecord.member.firstName, + userRecord.member.lastName, + userRecord.member.age, + userRecord.member.salary, + new DepartmentDTO( + userRecord.member.department.id, + userRecord.member.department.name, + userRecord.member.department.managerMemberId + ), + userRecord.member.joinedAt, + userRecord.member.phoneNumber, + userRecord.member.email, + userRecord.member.pr + ), + userRecord.isAdmin + ); + } +} diff --git a/server/src/users/shared/userMenuItem.ts b/server/src/modules/users/dtos/userMenuItemDTO.ts similarity index 100% rename from server/src/users/shared/userMenuItem.ts rename to server/src/modules/users/dtos/userMenuItemDTO.ts diff --git a/server/src/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService.ts b/server/src/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService.ts new file mode 100644 index 0000000..7427b1c --- /dev/null +++ b/server/src/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService.ts @@ -0,0 +1,45 @@ +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { USER_MENU_ITEM_ACTIONS } from '@/modules/auth/shared/constants/actions'; +import { UserMenuItemOrm } from '@/modules/auth/shared/createOso'; +import { Result } from '@/shared/core/result'; +import { UseCase } from '@/shared/core/useCase'; +import { PrismaClient } from '@prisma/client'; + +export type GetLoggedInUserInfoRequest = {}; +export type GetLoggedInUserInfoResponse = { + username: string; + userMenu: UserMenuItem[]; +}; +export type UserMenuItem = { + name: string; +}; + +export class GetLoggedInUserInfoService + implements + UseCase> +{ + constructor( + private readonly authorizer: Authorizer, + private readonly prisma: PrismaClient + ) {} + + async execute(): Promise> { + const query = await this.authorizer.authorizedQueryForUser( + USER_MENU_ITEM_ACTIONS.READ, + UserMenuItemOrm + ); + + const menuItems = await this.prisma.userMenuItem.findMany({ + select: { + name: true, + }, + where: query, + orderBy: { order: 'asc' }, + }); + + return Result.ok({ + username: this.authorizer.currentUser.username, + userMenu: menuItems, + }); + } +} diff --git a/server/src/resolvers.ts b/server/src/resolvers.ts deleted file mode 100644 index 5ee6e58..0000000 --- a/server/src/resolvers.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Resolvers } from './generated/resolver-types'; - -export const resolvers: Resolvers = { - Query: { - userInfo: async (_, __, context) => { - try { - const result = await context.getLoggedInUserInfoService.execute(); - return result; - } catch (err) { - console.error(err); - throw err; - } - }, - listAllMembers: async (_, __, { listAllMembersService }) => { - try { - const result = await listAllMembersService.execute(); - return result; - } catch (err) { - console.error(err); - throw err; - } - }, - showMemberDetail: async (_, { id }, { showMemberDetailService }) => { - try { - const result = await showMemberDetailService.execute({ memberId: id }); - return result; - } catch (err) { - console.error(err); - throw err; - } - }, - }, - Mutation: { - editMemberDetail: async (_, { input }, { editMemberDetailService }) => { - try { - const { id: memberId, ...payload } = input; - - const result = await editMemberDetailService.execute({ - memberId, - payload: { - ...(payload.age && { age: payload.age }), - ...(payload.departmentId && { departmentId: payload.departmentId }), - ...(payload.email && { email: payload.email }), - ...(payload.firstName && { firstName: payload.firstName }), - ...(payload.lastName && { lastName: payload.lastName }), - ...(payload.phoneNumber && { phoneNumber: payload.phoneNumber }), - ...(payload.pr && { pr: payload.pr }), - ...(payload.salary && { salary: payload.salary }), - }, - }); - return result; - } catch (err) { - console.error(err); - throw err; - } - }, - }, -}; diff --git a/server/src/shared/apiError.ts b/server/src/shared/apiError.ts deleted file mode 100644 index 99fd028..0000000 --- a/server/src/shared/apiError.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { AppError, ErrorCode, ErrorCodes } from './appError'; -import { NextFunction, Request, Response } from 'express'; - -const StatusCodeMap = { - [ErrorCodes.BAD_REQUEST]: 400, - [ErrorCodes.USER_NOT_FOUND]: 400, - [ErrorCodes.MEMBER_NOT_FOUND]: 400, -}; - -type StatusCodeMapKey = keyof typeof StatusCodeMap; -export class ApiError extends Error { - statusCode: number; - code: ErrorCode; - constructor(message: string, code: ErrorCode, statusCode: number) { - super(message); - Object.setPrototypeOf(this, ApiError.prototype); - - this.code = code; - this.statusCode = statusCode; - } - - static fromAppError(err: AppError) { - const { message, code } = err; - const errorCode = err.code as StatusCodeMapKey; - let statusCode = StatusCodeMap[errorCode]; - if (!statusCode) { - statusCode = 500; - } - return new ApiError(message, code, statusCode); - } -} - -export function apiErrorHandler( - err: Error, - req: Request, - res: Response, - next: NextFunction -) { - if (err instanceof AppError) { - const { statusCode, code, message } = ApiError.fromAppError(err); - return res.status(statusCode).json({ code: code, message: message }); - } - res.status(500).json({ message: 'something went wrong' }); -} diff --git a/server/src/shared/asyncHandler.ts b/server/src/shared/asyncHandler.ts deleted file mode 100644 index 439a571..0000000 --- a/server/src/shared/asyncHandler.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NextFunction, Request, RequestHandler, Response } from 'express'; - -export function asyncHandler(fn: RequestHandler) { - return async (req: Request, res: Response, next: NextFunction) => { - try { - await fn(req, res, next); - } catch (err) { - next(err); - } - }; -} diff --git a/server/src/shared/appError.ts b/server/src/shared/core/errors/appError.ts similarity index 83% rename from server/src/shared/appError.ts rename to server/src/shared/core/errors/appError.ts index 0f2af93..c97a627 100644 --- a/server/src/shared/appError.ts +++ b/server/src/shared/core/errors/appError.ts @@ -1,4 +1,4 @@ -export class AppError extends Error { +export abstract class AppError extends Error { code: ErrorCode; constructor(message: string, code: ErrorCode) { super(message); @@ -11,6 +11,7 @@ export class AppError extends Error { export const ErrorCodes = { BAD_REQUEST: 'bad_request', INVALID_OPERATION: 'invalid_operation', + INVALID_ARGUMENT: 'invalid_argument', USER_NOT_FOUND: 'user_not_found', MEMBER_NOT_FOUND: 'member_not_found', } as const; diff --git a/server/src/shared/core/errors/invalidOperationError.ts b/server/src/shared/core/errors/invalidOperationError.ts new file mode 100644 index 0000000..8e9f4ba --- /dev/null +++ b/server/src/shared/core/errors/invalidOperationError.ts @@ -0,0 +1,7 @@ +import { AppError, ErrorCodes } from './appError'; + +export class InvalidOperationError extends AppError { + constructor(message = 'invalid operation') { + super(message, ErrorCodes.INVALID_OPERATION); + } +} diff --git a/server/src/shared/core/errors/useCaseError.ts b/server/src/shared/core/errors/useCaseError.ts new file mode 100644 index 0000000..2d6fee4 --- /dev/null +++ b/server/src/shared/core/errors/useCaseError.ts @@ -0,0 +1,6 @@ +export abstract class UseCaseError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, UseCaseError.prototype); + } +} diff --git a/server/src/shared/core/result.ts b/server/src/shared/core/result.ts new file mode 100644 index 0000000..31a24df --- /dev/null +++ b/server/src/shared/core/result.ts @@ -0,0 +1,49 @@ +export class Result { + public isSuccess: boolean; + public isFailure: boolean; + public error!: Error; + private _value?: T; + + private constructor(isSuccess: boolean, error?: Error | null, value?: T) { + if (isSuccess && error) { + throw new Error(`InvalidOperation: A result cannot be + successful and contain an error`); + } + if (!isSuccess && !error) { + throw new Error(`InvalidOperation: A failing result + needs to contain an error message`); + } + + this.isSuccess = isSuccess; + this.isFailure = !isSuccess; + if (error) { + this.error = error; + } + this._value = value; + + Object.freeze(this); + } + + public getValue(): T { + if (!this.isSuccess) { + throw new Error(`Cant retrieve the value from a failed result.`); + } + + return this._value!; + } + + public static ok(value?: U): Result { + return new Result(true, null, value); + } + + public static fail(error: Error): Result { + return new Result(false, error); + } + + public static combine(results: Result[]): Result { + for (let result of results) { + if (result.isFailure) return result; + } + return Result.ok(); + } +} diff --git a/server/src/shared/core/useCase.ts b/server/src/shared/core/useCase.ts new file mode 100644 index 0000000..2c1ca12 --- /dev/null +++ b/server/src/shared/core/useCase.ts @@ -0,0 +1,5 @@ +import { Result } from './result'; + +export interface UseCase> { + execute(request?: Req): Promise | Res; +} diff --git a/server/src/database/constants.ts b/server/src/shared/infra/database/constants.ts similarity index 100% rename from server/src/database/constants.ts rename to server/src/shared/infra/database/constants.ts diff --git a/server/prisma/migrations/20220418120651_init/migration.sql b/server/src/shared/infra/database/prisma/migrations/20220418120651_init/migration.sql similarity index 100% rename from server/prisma/migrations/20220418120651_init/migration.sql rename to server/src/shared/infra/database/prisma/migrations/20220418120651_init/migration.sql diff --git a/server/prisma/migrations/20220418122916_user_user_menu_item/migration.sql b/server/src/shared/infra/database/prisma/migrations/20220418122916_user_user_menu_item/migration.sql similarity index 100% rename from server/prisma/migrations/20220418122916_user_user_menu_item/migration.sql rename to server/src/shared/infra/database/prisma/migrations/20220418122916_user_user_menu_item/migration.sql diff --git a/server/prisma/migrations/20220419001802_user_member_not_null/migration.sql b/server/src/shared/infra/database/prisma/migrations/20220419001802_user_member_not_null/migration.sql similarity index 100% rename from server/prisma/migrations/20220419001802_user_member_not_null/migration.sql rename to server/src/shared/infra/database/prisma/migrations/20220419001802_user_member_not_null/migration.sql diff --git a/server/prisma/migrations/migration_lock.toml b/server/src/shared/infra/database/prisma/migrations/migration_lock.toml similarity index 100% rename from server/prisma/migrations/migration_lock.toml rename to server/src/shared/infra/database/prisma/migrations/migration_lock.toml diff --git a/server/prisma/schema.prisma b/server/src/shared/infra/database/prisma/schema.prisma similarity index 100% rename from server/prisma/schema.prisma rename to server/src/shared/infra/database/prisma/schema.prisma diff --git a/server/prisma/seed.ts b/server/src/shared/infra/database/prisma/seed.ts similarity index 98% rename from server/prisma/seed.ts rename to server/src/shared/infra/database/prisma/seed.ts index e1fd814..3ec9518 100644 --- a/server/prisma/seed.ts +++ b/server/src/shared/infra/database/prisma/seed.ts @@ -1,4 +1,4 @@ -import { DEPARTMENT_IDS, USERS } from '../src/database/constants'; +import { DEPARTMENT_IDS, USERS } from '../constants'; import { PrismaClient, Prisma, Member, User } from '@prisma/client'; import faker from '@faker-js/faker'; const prisma = new PrismaClient(); diff --git a/server/src/shared/infra/http/__tests__/resolvers.test.ts b/server/src/shared/infra/http/__tests__/resolvers.test.ts new file mode 100644 index 0000000..9a154de --- /dev/null +++ b/server/src/shared/infra/http/__tests__/resolvers.test.ts @@ -0,0 +1,265 @@ +import { + GetLoggedInUserInfoResponse, + GetLoggedInUserInfoService, +} from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { Result } from '@/shared/core/result'; +import { + EditMemberDetailInput, + Mutation, + Query, +} from '@/shared/infra/http/generated/resolver-types'; +import { runQuery } from '@/../tests/helpers/graphql'; +import { MockedDependencies } from '@/../tests/helpers/dependencies'; +import { ListAllMembersResponse } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { displayableMemberFactory } from '@/../tests/factories/member'; +import { ShowMemberDetailResponse } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import faker from '@faker-js/faker'; + +describe('resolvers', () => { + let mockContainer: MockedDependencies; + + beforeEach(() => { + mockContainer = new MockedDependencies(); + mockContainer.clear(); + }); + + describe('Query', () => { + describe('userInfo', () => { + it('should call GetLoggedInUserInfoService', async () => { + const { dependencies } = mockContainer; + + const expectedResponse: GetLoggedInUserInfoResponse = { + username: 'test', + userMenu: [], + }; + + dependencies.getLoggedInUserInfoService.execute.mockResolvedValue( + Result.ok(expectedResponse) + ); + + const response = await runQuery( + { + query: ` + query { + userInfo { + username + userMenu { + name + } + } + } + `, + }, + { + dependencies, + } + ); + + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + userInfo: expectedResponse, + }); + }); + }); + + describe('listAllMembers', () => { + it('should call ListAllMembersService', async () => { + const { dependencies } = mockContainer; + + const expectedResponse: ListAllMembersResponse = { + members: displayableMemberFactory.buildList(2), + }; + + dependencies.listAllMembersService.execute.mockResolvedValue( + Result.ok(expectedResponse) + ); + + const response = await runQuery( + { + query: ` + query { + listAllMembers { + members { + id + avatar + firstName + lastName + age + salary + department { + id + name + managerMemberId + } + joinedAt + phoneNumber + email + pr + editable + isLoggedInUser + } + } + } + `, + }, + { + dependencies, + } + ); + + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + listAllMembers: { + members: expectedResponse.members.map((member) => ({ + ...member, + joinedAt: member.joinedAt?.toISOString(), + })), + }, + }); + }); + }); + + describe('showMemberDetail', () => { + it('should call ShowMemberDetailService', async () => { + const { dependencies } = mockContainer; + + const expectedResponse: ShowMemberDetailResponse = { + member: displayableMemberFactory.build(), + editableFields: [], + }; + + dependencies.showMemberDetailService.execute.mockResolvedValue( + Result.ok(expectedResponse) + ); + + const response = await runQuery( + { + query: ` + query { + showMemberDetail(id: "1") { + member { + id + avatar + firstName + lastName + age + salary + department { + id + name + managerMemberId + } + joinedAt + phoneNumber + email + pr + editable + isLoggedInUser + } + } + } + `, + }, + { + dependencies, + } + ); + + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + showMemberDetail: { + member: { + ...expectedResponse.member, + joinedAt: expectedResponse.member.joinedAt?.toISOString(), + }, + }, + }); + }); + }); + }); + + describe('Mutation', () => { + describe('editMemberDetail', () => { + it('should call editMemberDetailService with valid payload', async () => { + // Arrange + const { dependencies } = mockContainer; + dependencies.editMemberDetailService.execute.mockResolvedValue( + Result.ok({ result: true }) + ); + + // Act + const response = await runQuery( + { + query: ` + mutation ($input: EditMemberDetailInput!) { + editMemberDetail(input: $input) { + result + } + }`, + variables: { input: createEditMemberDetailInput() }, + }, + { + dependencies: dependencies, + } + ); + + // Assert + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + editMemberDetail: { + result: true, + }, + }); + }); + + it('should call editMemberDetailService with invalid payload', async () => { + // Arrange + const { dependencies } = mockContainer; + dependencies.editMemberDetailService.execute.mockResolvedValue( + Result.fail(new Error('invalid payload')) + ); + + // Act + const response = await runQuery( + { + query: ` + mutation ($input: EditMemberDetailInput!) { + editMemberDetail(input: $input) { + result + } + }`, + variables: { input: createEditMemberDetailInput() }, + }, + { + dependencies: dependencies, + } + ); + + // Assert + expect(response.status).toBe(200); + const executionResult = await response.json(); + expect(executionResult.data).toEqual({ + editMemberDetail: null, + }); + }); + }); + }); +}); + +function createEditMemberDetailInput(): EditMemberDetailInput { + return { + id: faker.datatype.uuid(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + age: faker.datatype.number(), + salary: faker.datatype.number(), + departmentId: faker.datatype.uuid(), + phoneNumber: faker.phone.phoneNumber(), + email: faker.internet.email(), + pr: faker.lorem.paragraph(), + }; +} diff --git a/server/src/shared/infra/http/app.ts b/server/src/shared/infra/http/app.ts new file mode 100644 index 0000000..60c56fa --- /dev/null +++ b/server/src/shared/infra/http/app.ts @@ -0,0 +1,98 @@ +import express, { Application } from 'express'; + +import 'reflect-metadata'; + +import { PrismaClient } from '@prisma/client'; +import { loadSchema } from '@graphql-tools/load'; +import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; +import { createContext } from './context'; +import { createYoga, createSchema } from 'graphql-yoga'; +import { createResolvers, Dependencies } from './resolvers'; +import { Authorizer } from '@/modules/auth/shared/authorizer'; +import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { PrismaMemberRepository } from '@/modules/members/infra/repos/prismaMemberRepository'; +import { ListAllMembersService } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import { EditMemberDetailService } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; +import { + createCoreOso, + createSqliteDataFilterOso, +} from '@/modules/auth/shared/createOso'; +import { createCheckLoggedInMiddleware } from '@/modules/auth/shared/checkLoggedInMiddleware'; + +export const prisma = new PrismaClient(); + +type UseCaseDependencies = { + authorizer: Authorizer; + prisma: PrismaClient; +}; + +const createUseCases = ({ authorizer, prisma }: UseCaseDependencies) => { + const getLoggedInUserInfoService = new GetLoggedInUserInfoService( + authorizer, + prisma + ); + + const prismaMemberRepository = new PrismaMemberRepository(authorizer, prisma); + + const listAllMembersService = new ListAllMembersService(authorizer, prisma); + const showMemberDetailService = new ShowMemberDetailService( + authorizer, + prisma + ); + const editMemberDetailService = new EditMemberDetailService( + authorizer, + prismaMemberRepository + ); + + return { + getLoggedInUserInfoService, + listAllMembersService, + showMemberDetailService, + editMemberDetailService, + }; +}; + +export async function startServer() { + const coreOso = await createCoreOso(); + const dataFilterOso = await createSqliteDataFilterOso(); + + const authorizer = new Authorizer(prisma, coreOso, dataFilterOso); + + // express + const app: Application = express(); + app.use(express.json()); + + const useCases = createUseCases({ + authorizer, + prisma, + }); + const graphQLServer = await createGraphQLServer(useCases); + + app.use('/graphql', createCheckLoggedInMiddleware(authorizer)); + + app.use('/graphql', graphQLServer.requestListener); + + const port: number = 3031; + app.listen(port, function () { + console.log(`App is listening on port ${port} !`); + }); +} + +export async function createGraphQLServer(deps: Dependencies) { + const schema = await loadSchema('schema.graphql', { + loaders: [new GraphQLFileLoader()], + }); + + const resolvers = createResolvers(deps); + const graphQLServer = createYoga({ + schema: createSchema({ + typeDefs: schema, + resolvers: resolvers, + }), + context: createContext(), + plugins: [], + }); + + return graphQLServer; +} diff --git a/server/src/shared/infra/http/context.ts b/server/src/shared/infra/http/context.ts new file mode 100644 index 0000000..d075407 --- /dev/null +++ b/server/src/shared/infra/http/context.ts @@ -0,0 +1,3 @@ +export const createContext = () => ({}); + +export interface Context {} diff --git a/server/src/generated/resolver-types.ts b/server/src/shared/infra/http/generated/resolver-types.ts similarity index 89% rename from server/src/generated/resolver-types.ts rename to server/src/shared/infra/http/generated/resolver-types.ts index 66de5b2..bdc9917 100644 --- a/server/src/generated/resolver-types.ts +++ b/server/src/shared/infra/http/generated/resolver-types.ts @@ -1,11 +1,11 @@ import { GraphQLResolveInfo, GraphQLScalarType, GraphQLScalarTypeConfig } from 'graphql'; -import { ListAllMembersResponse as ListAllMembersResponseModel } from '@/members/list-all-members/listAllMembersService'; -import { ShowMemberDetailResponse as ShowMemberDetailResponseModel } from '@/members/show-member-detail/showMemberDetailService'; -import { GetLoggedInUserInfoResponse as GetLoggedInUserInfoResponseModel, UserMenuItem as UserMenuItemModel } from '@/users/get-logged-in-user-info/getLoggedInUserInfoService'; -import { DisplayableMember as DisplayableMemberModel } from '@/members/shared/member'; -import { Department as DepartmentModel } from '@/members/shared/department'; -import { EditMemberDetailRequest as EditMemberDetailRequestModel, EditMemberDetailResponse as EditMemberDetailResponseModel } from '@/members/edit-member-detail/editMemberDetailService'; -import { Context } from '@/context'; +import { ListAllMembersResponse as ListAllMembersResponseModel } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { ShowMemberDetailResponse as ShowMemberDetailResponseModel } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import { GetLoggedInUserInfoResponse as GetLoggedInUserInfoResponseModel, UserMenuItem as UserMenuItemModel } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { DisplayableMember as DisplayableMemberModel } from '@/modules/members/dtos/displayableMemberDTO'; +import { DepartmentDTO as DepartmentDTOModel } from '@/modules/members/dtos/departmentDTO'; +import { EditMemberDetailRequest as EditMemberDetailRequestModel, EditMemberDetailResponse as EditMemberDetailResponseModel } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; +import { Context } from '@/shared/infra/http/context'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -70,7 +70,7 @@ export type Member = { export type Mutation = { __typename?: 'Mutation'; - editMemberDetail: EditMemberDetailResponse; + editMemberDetail?: Maybe; }; @@ -80,9 +80,9 @@ export type MutationEditMemberDetailArgs = { export type Query = { __typename?: 'Query'; - listAllMembers: ListAllMembersResponse; - showMemberDetail: ShowMemberDetailResponse; - userInfo: UserInfo; + listAllMembers?: Maybe; + showMemberDetail?: Maybe; + userInfo?: Maybe; }; @@ -178,7 +178,7 @@ export type DirectiveResolverFn; DateTime: ResolverTypeWrapper; - Department: ResolverTypeWrapper; + Department: ResolverTypeWrapper; EditMemberDetailInput: ResolverTypeWrapper; EditMemberDetailResponse: ResolverTypeWrapper; ID: ResolverTypeWrapper; @@ -197,7 +197,7 @@ export type ResolversTypes = { export type ResolversParentTypes = { Boolean: Scalars['Boolean']; DateTime: Scalars['DateTime']; - Department: DepartmentModel; + Department: DepartmentDTOModel; EditMemberDetailInput: EditMemberDetailRequestModel; EditMemberDetailResponse: EditMemberDetailResponseModel; ID: Scalars['ID']; @@ -251,13 +251,13 @@ export type MemberResolvers = { - editMemberDetail?: Resolver>; + editMemberDetail?: Resolver, ParentType, ContextType, RequireFields>; }; export type QueryResolvers = { - listAllMembers?: Resolver; - showMemberDetail?: Resolver>; - userInfo?: Resolver; + listAllMembers?: Resolver, ParentType, ContextType>; + showMemberDetail?: Resolver, ParentType, ContextType, RequireFields>; + userInfo?: Resolver, ParentType, ContextType>; }; export type ShowMemberDetailResponseResolvers = { diff --git a/server/src/shared/infra/http/resolvers.ts b/server/src/shared/infra/http/resolvers.ts new file mode 100644 index 0000000..797a8ef --- /dev/null +++ b/server/src/shared/infra/http/resolvers.ts @@ -0,0 +1,80 @@ +import { + editMemberDetailPayloadSchema, + EditMemberDetailService, +} from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; +import { ListAllMembersService } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; +import { Resolvers } from './generated/resolver-types'; + +export type Dependencies = { + getLoggedInUserInfoService: GetLoggedInUserInfoService; + listAllMembersService: ListAllMembersService; + showMemberDetailService: ShowMemberDetailService; + editMemberDetailService: EditMemberDetailService; +}; + +export const createResolvers = ({ + editMemberDetailService, + getLoggedInUserInfoService, + listAllMembersService, + showMemberDetailService, +}: Dependencies): Resolvers => { + return { + Query: { + userInfo: async () => { + const resultOrError = await getLoggedInUserInfoService.execute(); + if (resultOrError.isFailure) { + console.error(resultOrError.error); + // TODO: error handling + return null; + } + return resultOrError.getValue(); + }, + listAllMembers: async () => { + const resultOrError = await listAllMembersService.execute(); + if (resultOrError.isFailure) { + console.error(resultOrError.error); + // TODO: error handling + return null; + } + return resultOrError.getValue(); + }, + showMemberDetail: async (_, { id }) => { + const resultOrError = await showMemberDetailService.execute({ + memberId: id, + }); + if (resultOrError.isFailure) { + console.error(resultOrError.error); + // TODO: + return null; + } + return resultOrError.getValue(); + }, + }, + Mutation: { + editMemberDetail: async (_, { input }) => { + const { id: memberId, ...payload } = input; + + const parsedPayload = editMemberDetailPayloadSchema.safeParse(payload); + if (!parsedPayload.success) { + // TODO: error handling + console.error(parsedPayload.error); + return null; + } + + const resultOrError = await editMemberDetailService.execute({ + memberId, + payload: parsedPayload.data, + }); + if (resultOrError.isFailure) { + console.error(resultOrError.error); + // TODO: error handling + return null; + } + + return resultOrError.getValue(); + }, + }, + }; +}; diff --git a/server/src/users/get-logged-in-user-info/getLoggedInUserInfoRepository.ts b/server/src/users/get-logged-in-user-info/getLoggedInUserInfoRepository.ts deleted file mode 100644 index c0a686f..0000000 --- a/server/src/users/get-logged-in-user-info/getLoggedInUserInfoRepository.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { User } from '../shared/user'; - -export type UserInfo = { - userMenu: { name: string }[]; -}; -export interface GetLoggedInUserInfoRepository { - queryUserInfo(user: User): Promise; -} diff --git a/server/src/users/get-logged-in-user-info/getLoggedInUserInfoService.ts b/server/src/users/get-logged-in-user-info/getLoggedInUserInfoService.ts deleted file mode 100644 index f86aeb0..0000000 --- a/server/src/users/get-logged-in-user-info/getLoggedInUserInfoService.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Authorizer } from '@/auth/shared/authorizer'; -import { NotAuthorizedError } from '@/auth/shared/errors/not-authorized-error'; -import { GetLoggedInUserInfoRepository } from './getLoggedInUserInfoRepository'; - -export type GetLoggedInUserInfoRequest = {}; -export type GetLoggedInUserInfoResponse = { - username: string; - userMenu: UserMenuItem[]; -}; -export type UserMenuItem = { - name: string; -}; - -export class GetLoggedInUserInfoService { - constructor( - private readonly authorizer: Authorizer, - private readonly repository: GetLoggedInUserInfoRepository - ) {} - - async execute(): Promise { - try { - const userInfo = await this.repository.queryUserInfo( - this.authorizer.currentUser - ); - this.authorizer.currentUser.username; - return { - username: this.authorizer.currentUser.username, - userMenu: userInfo.userMenu, - }; - } catch (error) { - if (error instanceof NotAuthorizedError) { - return { - username: this.authorizer.currentUser.username, - userMenu: [], - }; - } - throw error; - } - } -} diff --git a/server/src/users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository.ts b/server/src/users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository.ts deleted file mode 100644 index 352cb08..0000000 --- a/server/src/users/get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { USER_MENU_ITEM_ACTIONS } from '@/auth/shared/constants/actions'; -import { UserMenuItemOrm } from '@/auth/shared/createOso'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { User } from '@/users/shared/user'; -import { PrismaClient } from '@prisma/client'; -import { - GetLoggedInUserInfoRepository, - UserInfo, -} from '../getLoggedInUserInfoRepository'; - -export class GetLoggedInUserInfoSqliteRepository - implements GetLoggedInUserInfoRepository -{ - constructor( - private readonly dataFilter: DataFilter, - private readonly prisma: PrismaClient - ) {} - - async queryUserInfo(user: User): Promise { - const query = await this.dataFilter.authorizedQuery( - user, - USER_MENU_ITEM_ACTIONS.READ, - UserMenuItemOrm - ); - - const menuItems = await this.prisma.userMenuItem.findMany({ - select: { - name: true, - }, - where: query, - orderBy: { order: 'asc' }, - }); - - return { - userMenu: menuItems, - }; - } -} diff --git a/server/src/users/shared/user.ts b/server/src/users/shared/user.ts deleted file mode 100644 index 502a659..0000000 --- a/server/src/users/shared/user.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Member } from '@/members/shared/member'; - -export class User { - constructor( - public id: string, - public username: string, - public memberInfo: Member, - public isAdmin: boolean - ) {} -} diff --git a/server/src/users/usersController.ts b/server/src/users/usersController.ts deleted file mode 100644 index 83e2857..0000000 --- a/server/src/users/usersController.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Request, Response } from 'express'; -import { GetLoggedInUserInfoService } from './get-logged-in-user-info/getLoggedInUserInfoService'; - -export class UsersController { - constructor( - private readonly getLoggedInUserInfoService: GetLoggedInUserInfoService - ) {} - - getLoggedInUserInfo = async (req: Request, res: Response) => { - const result = await this.getLoggedInUserInfoService.execute(); - res.json(result); - }; -} diff --git a/server/src/users/usersRouter.ts b/server/src/users/usersRouter.ts deleted file mode 100644 index 51cf627..0000000 --- a/server/src/users/usersRouter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import express from 'express'; -import { Authorizer } from '@/auth/shared/authorizer'; -import { createCheckLoggedInMiddleware } from '@/auth/check-logged-in/checkLoggedInMiddleware'; -import { UsersController } from './usersController'; -import { DataFilter } from '@/auth/shared/dataFilter'; -import { GetLoggedInUserInfoSqliteRepository } from './get-logged-in-user-info/repository/getLoggedInUserInfoSqliteRepository'; -import { GetLoggedInUserInfoService } from './get-logged-in-user-info/getLoggedInUserInfoService'; -import { asyncHandler } from '@/shared/asyncHandler'; -import { PrismaClient } from '@prisma/client'; - -type Dependencies = { - dataFilter: DataFilter; - authorizer: Authorizer; - prisma: PrismaClient; -}; - -export function createUsersRouter({ - dataFilter, - authorizer, - prisma, -}: Dependencies) { - const getLoggedInUserInfoRepository = new GetLoggedInUserInfoSqliteRepository( - dataFilter, - prisma - ); - const getLoggedInUserInfoService = new GetLoggedInUserInfoService( - authorizer, - getLoggedInUserInfoRepository - ); - - const usersController = new UsersController(getLoggedInUserInfoService); - const router = express.Router(); - - router.use(createCheckLoggedInMiddleware(authorizer)); - - router.get('/info', asyncHandler(usersController.getLoggedInUserInfo)); - - return router; -} diff --git a/server/tests/factories/member.ts b/server/tests/factories/member.ts new file mode 100644 index 0000000..503ff1b --- /dev/null +++ b/server/tests/factories/member.ts @@ -0,0 +1,25 @@ +import { DisplayableMember } from '@/modules/members/dtos/displayableMemberDTO'; +import { Factory } from 'fishery'; +import faker from '@faker-js/faker'; + +export const displayableMemberFactory = Factory.define( + () => ({ + id: faker.datatype.uuid(), + avatar: faker.image.avatar(), + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + age: faker.datatype.number({ min: 25, max: 60 }), + salary: faker.datatype.number({ min: 40000, max: 90000 }), + joinedAt: faker.date.past(), + phoneNumber: faker.phone.phoneNumber('###-####-####'), + email: faker.internet.exampleEmail(), + pr: faker.lorem.sentence(), + department: { + id: faker.datatype.uuid(), + name: faker.name.jobArea(), + managerMemberId: faker.datatype.uuid(), + }, + editable: faker.datatype.boolean(), + isLoggedInUser: faker.datatype.boolean(), + }) +); diff --git a/server/tests/helpers/dependencies.ts b/server/tests/helpers/dependencies.ts new file mode 100644 index 0000000..324313d --- /dev/null +++ b/server/tests/helpers/dependencies.ts @@ -0,0 +1,57 @@ +import { ListAllMembersService } from '@/modules/members/useCases/query/listAllMembers/listAllMembersService'; +import { ShowMemberDetailService } from '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService'; +import { EditMemberDetailService } from '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService'; +import { GetLoggedInUserInfoService } from '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService'; + +jest.mock( + '@/modules/members/useCases/query/listAllMembers/listAllMembersService' +); +jest.mock( + '@/modules/members/useCases/query/showMemberDetail/showMemberDetailService' +); +jest.mock( + '@/modules/members/useCases/command/editMemberDetail/editMemberDetailService' +); +jest.mock( + '@/modules/users/useCases/query/getLoggedInUserInfo/getLoggedInUserInfoService' +); + +const MockedListAllMembersService = ListAllMembersService as jest.Mock; +const MockedShowMemberDetailService = ShowMemberDetailService as jest.Mock; +const MockedEditMemberDetailService = EditMemberDetailService as jest.Mock; +const MockedGetLoggedInUserInfoService = + GetLoggedInUserInfoService as jest.Mock; + +export class MockedDependencies { + private readonly getLoggedInUserInfoService: jest.Mocked; + private readonly listAllMembersService: jest.Mocked; + private readonly showMemberDetailService: jest.Mocked; + private readonly editMemberDetailService: jest.Mocked; + + constructor() { + this.getLoggedInUserInfoService = + new MockedGetLoggedInUserInfoService() as jest.Mocked; + this.listAllMembersService = + new MockedListAllMembersService() as jest.Mocked; + this.showMemberDetailService = + new MockedShowMemberDetailService() as jest.Mocked; + this.editMemberDetailService = + new MockedEditMemberDetailService() as jest.Mocked; + } + + get dependencies() { + return { + getLoggedInUserInfoService: this.getLoggedInUserInfoService, + listAllMembersService: this.listAllMembersService, + showMemberDetailService: this.showMemberDetailService, + editMemberDetailService: this.editMemberDetailService, + }; + } + + clear() { + MockedGetLoggedInUserInfoService.mockClear(); + MockedListAllMembersService.mockClear(); + MockedShowMemberDetailService.mockClear(); + MockedEditMemberDetailService.mockClear(); + } +} diff --git a/server/tests/helpers/graphql.ts b/server/tests/helpers/graphql.ts new file mode 100644 index 0000000..367cbd2 --- /dev/null +++ b/server/tests/helpers/graphql.ts @@ -0,0 +1,36 @@ +import { createGraphQLServer } from '@/shared/infra/http/app'; +import { Dependencies } from '@/shared/infra/http/resolvers'; +import { ExecutionResult } from 'graphql'; + +interface RunQueryBody { + query: string; + variables?: Record; +} + +interface RunQueryOptions { + dependencies: Dependencies; +} + +interface RunQueryResponse extends Response { + json(): Promise; +} + +export async function runQuery( + body: RunQueryBody, + opts: RunQueryOptions +): Promise>> { + const server = await createGraphQLServer(opts.dependencies); + + const response = await server.fetch('/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query: body.query, + variables: body.variables, + }), + }); + + return response; +} diff --git a/server/tests/helpers/request.ts b/server/tests/helpers/request.ts index 5758bbd..f333604 100644 --- a/server/tests/helpers/request.ts +++ b/server/tests/helpers/request.ts @@ -1,5 +1,5 @@ +import { USERS } from '@/shared/infra/database/constants'; import request from 'supertest'; -import { USERS } from '@/database/constants'; export function authorizeRequest( req: request.Test,