From d9702993b6ddcbe560f8709abbd47b9380f605a7 Mon Sep 17 00:00:00 2001 From: istiopaxx Date: Sun, 21 May 2023 22:30:48 +0900 Subject: [PATCH 1/2] add grpc python server --- image-process/.gitignore | 163 ++++++++++++++++++++++++ image-process/barcode.py | 17 --- image-process/docker-compose.yml | 4 +- image-process/image-process.proto | 33 +++++ image-process/image_process.py | 60 +++++++++ image-process/image_process_pb2.py | 26 ++-- image-process/image_process_pb2.pyi | 8 +- image-process/image_process_pb2_grpc.py | 16 +-- image-process/proto-compile.sh | 1 + protos/image-process.proto | 6 +- protos/sync-protos.sh | 6 + 11 files changed, 294 insertions(+), 46 deletions(-) create mode 100644 image-process/.gitignore delete mode 100644 image-process/barcode.py create mode 100644 image-process/image-process.proto create mode 100644 image-process/image_process.py create mode 100644 image-process/proto-compile.sh create mode 100644 protos/sync-protos.sh diff --git a/image-process/.gitignore b/image-process/.gitignore new file mode 100644 index 0000000..ad12910 --- /dev/null +++ b/image-process/.gitignore @@ -0,0 +1,163 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + +# VSCode +.vscode/ diff --git a/image-process/barcode.py b/image-process/barcode.py deleted file mode 100644 index a7f68a2..0000000 --- a/image-process/barcode.py +++ /dev/null @@ -1,17 +0,0 @@ -from urllib.request import urlopen -from PIL import Image -from pyzbar.pyzbar import decode - - -def barcode_read(url): - img = Image.open(urlopen(url)) - print(img) - - detected_barcodes = decode(img) - - return detected_barcodes - - -if __name__ == '__main__': - url = 'https://cdn.foodnews.co.kr/news/photo/201702/62159_17720_2312.jpg' - print(barcode_read(url)) diff --git a/image-process/docker-compose.yml b/image-process/docker-compose.yml index cf36598..674e1f2 100644 --- a/image-process/docker-compose.yml +++ b/image-process/docker-compose.yml @@ -6,8 +6,8 @@ services: context: . dockerfile: ./Dockerfile-dev - # ports: - # - '9988:9988' + ports: + - '50051:50051' # command: # - python serve.py tty: true diff --git a/image-process/image-process.proto b/image-process/image-process.proto new file mode 100644 index 0000000..1f8e637 --- /dev/null +++ b/image-process/image-process.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package image_process; + +service ImageProcess { + rpc GetBarcodeInfoFromUrl(ImageInfo) returns (BarcodeInfos) {} +} + +message ImageInfo { + string image_url = 1; +} + +message BarcodeInfos { + message BarcodeInfo { + message Rect { + int32 left = 1; + int32 top = 2; + int32 width = 3; + int32 height = 4; + } + message Point { + int32 x = 1; + int32 y = 2; + } + string data = 1; + string type = 2; + Rect rect = 3; + repeated Point polygon = 4; + string orientation = 5; + int32 quality = 6; + } + repeated BarcodeInfo barcode_infos = 1; +} diff --git a/image-process/image_process.py b/image-process/image_process.py new file mode 100644 index 0000000..c4152b8 --- /dev/null +++ b/image-process/image_process.py @@ -0,0 +1,60 @@ + +from concurrent import futures +import logging + +import grpc +import image_process_pb2 +import image_process_pb2_grpc + +from urllib.request import urlopen +from PIL import Image +from pyzbar.pyzbar import decode + + +def get_barcode_info_from_url(url: str) -> list: + img = Image.open(urlopen(url)) + + detected_barcodes = decode(img) + print(detected_barcodes[0]) + + return image_process_pb2.BarcodeInfos(barcode_infos=list(map(lambda barcode_info: image_process_pb2.BarcodeInfos.BarcodeInfo( + data=barcode_info.data.decode('utf-8'), + type=barcode_info.type, + rect=image_process_pb2.BarcodeInfos.BarcodeInfo.Rect(left=barcode_info.rect.left, + top=barcode_info.rect.top, + width=barcode_info.rect.width, + height=barcode_info.rect.height), + polygon=list(map(lambda point: image_process_pb2.BarcodeInfos.BarcodeInfo.Point( + x=point.x, y=point.y), barcode_info.polygon)), + orientation=barcode_info.orientation, + quality=barcode_info.quality + ), detected_barcodes))) + + +class ImageProcessServicer(image_process_pb2_grpc.ImageProcessServicer): + def __init__(self): + pass + + def GetBarcodeInfoFromUrl(self, request, context): + print('request: ', request) + if request: + print('not null, request.image_url: ', request.image_url) + barcode_infos = get_barcode_info_from_url(request.image_url) + + if len(barcode_infos.barcode_infos) == 0: + return None + return barcode_infos + + +def serve(): + server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) + image_process_pb2_grpc.add_ImageProcessServicer_to_server( + ImageProcessServicer(), server) + server.add_insecure_port('[::]:50051') + server.start() + server.wait_for_termination() + + +if __name__ == '__main__': + logging.basicConfig() + serve() diff --git a/image-process/image_process_pb2.py b/image-process/image_process_pb2.py index f411b1f..f21b27d 100644 --- a/image-process/image_process_pb2.py +++ b/image-process/image_process_pb2.py @@ -13,23 +13,23 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13image-process.proto\"\x1e\n\tImageInfo\x12\x11\n\timage_url\x18\x01 \x01(\t\"\xd2\x02\n\x0c\x42\x61rcodeInfos\x12\x30\n\rbarcode_infos\x18\x01 \x03(\x0b\x32\x19.BarcodeInfos.BarcodeInfo\x1a\x8f\x02\n\x0b\x42\x61rcodeInfo\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12,\n\x04rect\x18\x03 \x01(\x0b\x32\x1e.BarcodeInfos.BarcodeInfo.Rect\x12/\n\x06points\x18\x04 \x03(\x0b\x32\x1f.BarcodeInfos.BarcodeInfo.Point\x12\x13\n\x0borientation\x18\x05 \x01(\t\x12\x0f\n\x07quality\x18\x06 \x01(\x05\x1a@\n\x04Rect\x12\x0c\n\x04left\x18\x01 \x01(\x05\x12\x0b\n\x03top\x18\x02 \x01(\x05\x12\r\n\x05width\x18\x03 \x01(\x05\x12\x0e\n\x06height\x18\x04 \x01(\x05\x1a\x1d\n\x05Point\x12\t\n\x01x\x18\x01 \x01(\x05\x12\t\n\x01y\x18\x02 \x01(\x05\x32=\n\x0cImageProcess\x12-\n\x0egetBarcodeInfo\x12\n.ImageInfo\x1a\r.BarcodeInfos\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13image-process.proto\x12\rimage_process\"\x1e\n\tImageInfo\x12\x11\n\timage_url\x18\x01 \x01(\t\"\xfd\x02\n\x0c\x42\x61rcodeInfos\x12>\n\rbarcode_infos\x18\x01 \x03(\x0b\x32\'.image_process.BarcodeInfos.BarcodeInfo\x1a\xac\x02\n\x0b\x42\x61rcodeInfo\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12:\n\x04rect\x18\x03 \x01(\x0b\x32,.image_process.BarcodeInfos.BarcodeInfo.Rect\x12>\n\x07polygon\x18\x04 \x03(\x0b\x32-.image_process.BarcodeInfos.BarcodeInfo.Point\x12\x13\n\x0borientation\x18\x05 \x01(\t\x12\x0f\n\x07quality\x18\x06 \x01(\x05\x1a@\n\x04Rect\x12\x0c\n\x04left\x18\x01 \x01(\x05\x12\x0b\n\x03top\x18\x02 \x01(\x05\x12\r\n\x05width\x18\x03 \x01(\x05\x12\x0e\n\x06height\x18\x04 \x01(\x05\x1a\x1d\n\x05Point\x12\t\n\x01x\x18\x01 \x01(\x05\x12\t\n\x01y\x18\x02 \x01(\x05\x32`\n\x0cImageProcess\x12P\n\x15GetBarcodeInfoFromUrl\x12\x18.image_process.ImageInfo\x1a\x1b.image_process.BarcodeInfos\"\x00\x62\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'image_process_pb2', globals()) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _IMAGEINFO._serialized_start=23 - _IMAGEINFO._serialized_end=53 - _BARCODEINFOS._serialized_start=56 - _BARCODEINFOS._serialized_end=394 - _BARCODEINFOS_BARCODEINFO._serialized_start=123 - _BARCODEINFOS_BARCODEINFO._serialized_end=394 - _BARCODEINFOS_BARCODEINFO_RECT._serialized_start=299 - _BARCODEINFOS_BARCODEINFO_RECT._serialized_end=363 - _BARCODEINFOS_BARCODEINFO_POINT._serialized_start=365 - _BARCODEINFOS_BARCODEINFO_POINT._serialized_end=394 - _IMAGEPROCESS._serialized_start=396 - _IMAGEPROCESS._serialized_end=457 + _IMAGEINFO._serialized_start=38 + _IMAGEINFO._serialized_end=68 + _BARCODEINFOS._serialized_start=71 + _BARCODEINFOS._serialized_end=452 + _BARCODEINFOS_BARCODEINFO._serialized_start=152 + _BARCODEINFOS_BARCODEINFO._serialized_end=452 + _BARCODEINFOS_BARCODEINFO_RECT._serialized_start=357 + _BARCODEINFOS_BARCODEINFO_RECT._serialized_end=421 + _BARCODEINFOS_BARCODEINFO_POINT._serialized_start=423 + _BARCODEINFOS_BARCODEINFO_POINT._serialized_end=452 + _IMAGEPROCESS._serialized_start=454 + _IMAGEPROCESS._serialized_end=550 # @@protoc_insertion_point(module_scope) diff --git a/image-process/image_process_pb2.pyi b/image-process/image_process_pb2.pyi index 51e0d4e..73560f0 100644 --- a/image-process/image_process_pb2.pyi +++ b/image-process/image_process_pb2.pyi @@ -8,7 +8,7 @@ DESCRIPTOR: _descriptor.FileDescriptor class BarcodeInfos(_message.Message): __slots__ = ["barcode_infos"] class BarcodeInfo(_message.Message): - __slots__ = ["data", "orientation", "points", "quality", "rect", "type"] + __slots__ = ["data", "orientation", "polygon", "quality", "rect", "type"] class Point(_message.Message): __slots__ = ["x", "y"] X_FIELD_NUMBER: _ClassVar[int] @@ -29,17 +29,17 @@ class BarcodeInfos(_message.Message): def __init__(self, left: _Optional[int] = ..., top: _Optional[int] = ..., width: _Optional[int] = ..., height: _Optional[int] = ...) -> None: ... DATA_FIELD_NUMBER: _ClassVar[int] ORIENTATION_FIELD_NUMBER: _ClassVar[int] - POINTS_FIELD_NUMBER: _ClassVar[int] + POLYGON_FIELD_NUMBER: _ClassVar[int] QUALITY_FIELD_NUMBER: _ClassVar[int] RECT_FIELD_NUMBER: _ClassVar[int] TYPE_FIELD_NUMBER: _ClassVar[int] data: str orientation: str - points: _containers.RepeatedCompositeFieldContainer[BarcodeInfos.BarcodeInfo.Point] + polygon: _containers.RepeatedCompositeFieldContainer[BarcodeInfos.BarcodeInfo.Point] quality: int rect: BarcodeInfos.BarcodeInfo.Rect type: str - def __init__(self, data: _Optional[str] = ..., type: _Optional[str] = ..., rect: _Optional[_Union[BarcodeInfos.BarcodeInfo.Rect, _Mapping]] = ..., points: _Optional[_Iterable[_Union[BarcodeInfos.BarcodeInfo.Point, _Mapping]]] = ..., orientation: _Optional[str] = ..., quality: _Optional[int] = ...) -> None: ... + def __init__(self, data: _Optional[str] = ..., type: _Optional[str] = ..., rect: _Optional[_Union[BarcodeInfos.BarcodeInfo.Rect, _Mapping]] = ..., polygon: _Optional[_Iterable[_Union[BarcodeInfos.BarcodeInfo.Point, _Mapping]]] = ..., orientation: _Optional[str] = ..., quality: _Optional[int] = ...) -> None: ... BARCODE_INFOS_FIELD_NUMBER: _ClassVar[int] barcode_infos: _containers.RepeatedCompositeFieldContainer[BarcodeInfos.BarcodeInfo] def __init__(self, barcode_infos: _Optional[_Iterable[_Union[BarcodeInfos.BarcodeInfo, _Mapping]]] = ...) -> None: ... diff --git a/image-process/image_process_pb2_grpc.py b/image-process/image_process_pb2_grpc.py index 9e690cc..7761db0 100644 --- a/image-process/image_process_pb2_grpc.py +++ b/image-process/image_process_pb2_grpc.py @@ -14,8 +14,8 @@ def __init__(self, channel): Args: channel: A grpc.Channel. """ - self.getBarcodeInfo = channel.unary_unary( - '/ImageProcess/getBarcodeInfo', + self.GetBarcodeInfoFromUrl = channel.unary_unary( + '/image_process.ImageProcess/GetBarcodeInfoFromUrl', request_serializer=image__process__pb2.ImageInfo.SerializeToString, response_deserializer=image__process__pb2.BarcodeInfos.FromString, ) @@ -24,7 +24,7 @@ def __init__(self, channel): class ImageProcessServicer(object): """Missing associated documentation comment in .proto file.""" - def getBarcodeInfo(self, request, context): + def GetBarcodeInfoFromUrl(self, request, context): """Missing associated documentation comment in .proto file.""" context.set_code(grpc.StatusCode.UNIMPLEMENTED) context.set_details('Method not implemented!') @@ -33,14 +33,14 @@ def getBarcodeInfo(self, request, context): def add_ImageProcessServicer_to_server(servicer, server): rpc_method_handlers = { - 'getBarcodeInfo': grpc.unary_unary_rpc_method_handler( - servicer.getBarcodeInfo, + 'GetBarcodeInfoFromUrl': grpc.unary_unary_rpc_method_handler( + servicer.GetBarcodeInfoFromUrl, request_deserializer=image__process__pb2.ImageInfo.FromString, response_serializer=image__process__pb2.BarcodeInfos.SerializeToString, ), } generic_handler = grpc.method_handlers_generic_handler( - 'ImageProcess', rpc_method_handlers) + 'image_process.ImageProcess', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) @@ -49,7 +49,7 @@ class ImageProcess(object): """Missing associated documentation comment in .proto file.""" @staticmethod - def getBarcodeInfo(request, + def GetBarcodeInfoFromUrl(request, target, options=(), channel_credentials=None, @@ -59,7 +59,7 @@ def getBarcodeInfo(request, wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/ImageProcess/getBarcodeInfo', + return grpc.experimental.unary_unary(request, target, '/image_process.ImageProcess/GetBarcodeInfoFromUrl', image__process__pb2.ImageInfo.SerializeToString, image__process__pb2.BarcodeInfos.FromString, options, channel_credentials, diff --git a/image-process/proto-compile.sh b/image-process/proto-compile.sh new file mode 100644 index 0000000..bb79f42 --- /dev/null +++ b/image-process/proto-compile.sh @@ -0,0 +1 @@ +python -m grpc_tools.protoc -I. --python_out=. --pyi_out=. --grpc_python_out=. ./image-process.proto \ No newline at end of file diff --git a/protos/image-process.proto b/protos/image-process.proto index 1dd2058..1f8e637 100644 --- a/protos/image-process.proto +++ b/protos/image-process.proto @@ -1,7 +1,9 @@ syntax = "proto3"; +package image_process; + service ImageProcess { - rpc getBarcodeInfo(ImageInfo) returns (BarcodeInfos) {} + rpc GetBarcodeInfoFromUrl(ImageInfo) returns (BarcodeInfos) {} } message ImageInfo { @@ -23,7 +25,7 @@ message BarcodeInfos { string data = 1; string type = 2; Rect rect = 3; - repeated Point points = 4; + repeated Point polygon = 4; string orientation = 5; int32 quality = 6; } diff --git a/protos/sync-protos.sh b/protos/sync-protos.sh new file mode 100644 index 0000000..5ad371f --- /dev/null +++ b/protos/sync-protos.sh @@ -0,0 +1,6 @@ +rm -f ../api/apps/api/src/*.proto ../api/apps/api-bundled/src/*.proto +cp ./*.proto ../api/apps/api/src +cp ./*.proto ../api/apps/api-bundled/src + +rm -f ../image-process/protos/*.proto +cp ./*.proto ../image-process From e804d490a26bb2db6b84a8700714f0e1d3d009cd Mon Sep 17 00:00:00 2001 From: istiopaxx Date: Sun, 21 May 2023 22:31:30 +0900 Subject: [PATCH 2/2] add api server grpc --- api/apps/api-bundled/src/app.module.ts | 6 + api/apps/api-bundled/src/image-process.proto | 33 +++ api/apps/api/src/app.module.ts | 6 + api/apps/api/src/image-process.proto | 33 +++ api/libs/ingredient/src/ingredient.module.ts | 20 ++ .../src/services/user-ingredient.service.ts | 31 ++- api/nest-cli.json | 4 + api/package-lock.json | 208 ++++++++++++++++-- api/package.json | 4 + 9 files changed, 326 insertions(+), 19 deletions(-) create mode 100644 api/apps/api-bundled/src/image-process.proto create mode 100644 api/apps/api/src/image-process.proto diff --git a/api/apps/api-bundled/src/app.module.ts b/api/apps/api-bundled/src/app.module.ts index 54ea587..080d45b 100644 --- a/api/apps/api-bundled/src/app.module.ts +++ b/api/apps/api-bundled/src/app.module.ts @@ -6,6 +6,9 @@ import { AuthModule } from '@app/auth/auth.module'; import { ImageModule } from '@app/image/image.module'; import { IngredientModule } from '@app/ingredient/ingredient.module'; import { RecipeModule } from '@app/recipe/recipe.module'; +import { AopModule } from '@app/common/aop/aop.module'; +import { CacheModule } from '@app/common/cache/cache.module'; +import { LogModule } from '@app/common/log/log.module'; @Module({ imports: [ @@ -20,6 +23,9 @@ import { RecipeModule } from '@app/recipe/recipe.module'; isGlobal: true, envFilePath: process.env.NODE_ENV === 'test' ? '.env.test.' : '.env.dev', }), + AopModule, + CacheModule, + LogModule, UserModule, AuthModule, ImageModule, diff --git a/api/apps/api-bundled/src/image-process.proto b/api/apps/api-bundled/src/image-process.proto new file mode 100644 index 0000000..1f8e637 --- /dev/null +++ b/api/apps/api-bundled/src/image-process.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package image_process; + +service ImageProcess { + rpc GetBarcodeInfoFromUrl(ImageInfo) returns (BarcodeInfos) {} +} + +message ImageInfo { + string image_url = 1; +} + +message BarcodeInfos { + message BarcodeInfo { + message Rect { + int32 left = 1; + int32 top = 2; + int32 width = 3; + int32 height = 4; + } + message Point { + int32 x = 1; + int32 y = 2; + } + string data = 1; + string type = 2; + Rect rect = 3; + repeated Point polygon = 4; + string orientation = 5; + int32 quality = 6; + } + repeated BarcodeInfo barcode_infos = 1; +} diff --git a/api/apps/api/src/app.module.ts b/api/apps/api/src/app.module.ts index 54ea587..080d45b 100644 --- a/api/apps/api/src/app.module.ts +++ b/api/apps/api/src/app.module.ts @@ -6,6 +6,9 @@ import { AuthModule } from '@app/auth/auth.module'; import { ImageModule } from '@app/image/image.module'; import { IngredientModule } from '@app/ingredient/ingredient.module'; import { RecipeModule } from '@app/recipe/recipe.module'; +import { AopModule } from '@app/common/aop/aop.module'; +import { CacheModule } from '@app/common/cache/cache.module'; +import { LogModule } from '@app/common/log/log.module'; @Module({ imports: [ @@ -20,6 +23,9 @@ import { RecipeModule } from '@app/recipe/recipe.module'; isGlobal: true, envFilePath: process.env.NODE_ENV === 'test' ? '.env.test.' : '.env.dev', }), + AopModule, + CacheModule, + LogModule, UserModule, AuthModule, ImageModule, diff --git a/api/apps/api/src/image-process.proto b/api/apps/api/src/image-process.proto new file mode 100644 index 0000000..1f8e637 --- /dev/null +++ b/api/apps/api/src/image-process.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package image_process; + +service ImageProcess { + rpc GetBarcodeInfoFromUrl(ImageInfo) returns (BarcodeInfos) {} +} + +message ImageInfo { + string image_url = 1; +} + +message BarcodeInfos { + message BarcodeInfo { + message Rect { + int32 left = 1; + int32 top = 2; + int32 width = 3; + int32 height = 4; + } + message Point { + int32 x = 1; + int32 y = 2; + } + string data = 1; + string type = 2; + Rect rect = 3; + repeated Point polygon = 4; + string orientation = 5; + int32 quality = 6; + } + repeated BarcodeInfo barcode_infos = 1; +} diff --git a/api/libs/ingredient/src/ingredient.module.ts b/api/libs/ingredient/src/ingredient.module.ts index 8658ed8..4912901 100644 --- a/api/libs/ingredient/src/ingredient.module.ts +++ b/api/libs/ingredient/src/ingredient.module.ts @@ -8,6 +8,8 @@ import { import { UserIngredientRepository } from './repositories/user-ingredient.repository'; import { UserIngredientService } from './services/user-ingredient.service'; import { ConfigService } from '@nestjs/config'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { join } from 'path'; @Module({ imports: [ @@ -18,6 +20,24 @@ import { ConfigService } from '@nestjs/config'; inject: [ConfigService], }, ]), + ClientsModule.registerAsync([ + { + name: 'IMAGE_PROCESS_SERVICE', + useFactory: () => { + return { + transport: Transport.GRPC, + options: { + package: 'image_process', + url: 'localhost:50051', + protoPath: join(__dirname, '../../../image-process.proto'), + loader: { + keepCase: true, + }, + }, + }; + }, + }, + ]), ], controllers: [UserIngredientController], providers: [UserIngredientService, UserIngredientRepository], diff --git a/api/libs/ingredient/src/services/user-ingredient.service.ts b/api/libs/ingredient/src/services/user-ingredient.service.ts index 42922a1..c376f95 100644 --- a/api/libs/ingredient/src/services/user-ingredient.service.ts +++ b/api/libs/ingredient/src/services/user-ingredient.service.ts @@ -1,5 +1,12 @@ import { Logable } from '@app/common/log/log.decorator'; -import { Injectable, NotFoundException } from '@nestjs/common'; +import { + Inject, + Injectable, + NotFoundException, + OnModuleInit, +} from '@nestjs/common'; +import { ClientGrpc } from '@nestjs/microservices'; +import { firstValueFrom, lastValueFrom, map, Observable } from 'rxjs'; import { FilterUserIngredientDto } from '../dto/filter-ingredient.dto'; import { CreateUserIngredientDto, @@ -8,13 +15,33 @@ import { import { UserIngredient } from '../entities/user-ingredient.entity'; import { UserIngredientRepository } from '../repositories/user-ingredient.repository'; +interface ImageProcessService { + getBarcodeInfoFromUrl(imageInfo: { image_url: string }): Observable; +} + @Logable() @Injectable() -export class UserIngredientService { +export class UserIngredientService implements OnModuleInit { + private imageProcessService: ImageProcessService; constructor( private readonly userIngredientRepository: UserIngredientRepository, + @Inject('IMAGE_PROCESS_SERVICE') private readonly client: ClientGrpc, ) {} + onModuleInit() { + this.imageProcessService = + this.client.getService('ImageProcess'); + } + + getIngredientInfoFromImage(imageUrl: string) { + console.log('image_url: ', imageUrl); + const ret = this.imageProcessService.getBarcodeInfoFromUrl({ + image_url: imageUrl, + }); + console.log('ret: ', ret); + return ret; + } + async create( createUserIngredientDto: CreateUserIngredientDto, ): Promise { diff --git a/api/nest-cli.json b/api/nest-cli.json index 43e6f3b..f1bbc6b 100644 --- a/api/nest-cli.json +++ b/api/nest-cli.json @@ -11,6 +11,8 @@ "entryFile": "main", "sourceRoot": "apps/api/src", "compilerOptions": { + "assets": ["**/*.proto"], + "watchAssets": true, "plugins": [ { "name": "@nestjs/swagger", @@ -29,6 +31,8 @@ "entryFile": "main", "sourceRoot": "apps/api-bundled/src", "compilerOptions": { + "assets": ["**/*.proto"], + "watchAssets": true, "tsConfigPath": "apps/api-bundled/tsconfig.app.json", "webpack": true } diff --git a/api/package-lock.json b/api/package-lock.json index db08e32..6bc4a28 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -10,6 +10,8 @@ "license": "UNLICENSED", "dependencies": { "@aws-sdk/client-s3": "^3.301.0", + "@grpc/grpc-js": "^1.8.14", + "@grpc/proto-loader": "^0.7.7", "@nestjs/axios": "^2.0.0", "@nestjs/cache-manager": "^1.0.0", "@nestjs/common": "^9.0.0", @@ -17,6 +19,7 @@ "@nestjs/core": "^9.0.0", "@nestjs/jwt": "^10.0.2", "@nestjs/mapped-types": "^1.2.2", + "@nestjs/microservices": "^9.4.1", "@nestjs/mongoose": "^9.2.2", "@nestjs/passport": "^9.0.3", "@nestjs/platform-express": "^9.0.0", @@ -2200,6 +2203,36 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.8.14", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.8.14.tgz", + "integrity": "sha512-w84maJ6CKl5aApCMzFll0hxtFNT6or9WwMslobKaqWUEf1K+zhlL43bSQhFreyYWIWR+Z0xnVFC1KtLm4ZpM/A==", + "dependencies": { + "@grpc/proto-loader": "^0.7.0", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.7.tgz", + "integrity": "sha512-1TIeXOi8TuSCQprPItwoMymZXxWT0CPxUhkrkeCUH+D8U7QDwQ6b7SUz2MaLuWM2llT+J/TVFLmQI5KtML3BhQ==", + "dependencies": { + "@types/long": "^4.0.1", + "lodash.camelcase": "^4.3.0", + "long": "^4.0.0", + "protobufjs": "^7.0.0", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.8", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz", @@ -2956,6 +2989,63 @@ } } }, + "node_modules/@nestjs/microservices": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-9.4.1.tgz", + "integrity": "sha512-052P1gFdbjEnqTLY9mwhNg7Duy/6o6tLpUQHs5w3VhpBo/fwL/2n61PfG27UM57wwtDea49KnoXWJ0+bXubrqw==", + "dependencies": { + "iterare": "1.2.1", + "tslib": "2.5.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@grpc/grpc-js": "*", + "@nestjs/common": "^9.0.0", + "@nestjs/core": "^9.0.0", + "@nestjs/websockets": "^9.0.0", + "amqp-connection-manager": "*", + "amqplib": "*", + "cache-manager": "*", + "ioredis": "*", + "kafkajs": "*", + "mqtt": "*", + "nats": "*", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@grpc/grpc-js": { + "optional": true + }, + "@nestjs/websockets": { + "optional": true + }, + "amqp-connection-manager": { + "optional": true + }, + "amqplib": { + "optional": true + }, + "cache-manager": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "kafkajs": { + "optional": true + }, + "mqtt": { + "optional": true + }, + "nats": { + "optional": true + } + } + }, "node_modules/@nestjs/mongoose": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/@nestjs/mongoose/-/mongoose-9.2.2.tgz", @@ -3213,6 +3303,60 @@ "npm": ">=5.0.0" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, "node_modules/@sinclair/typebox": { "version": "0.24.51", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", @@ -3457,6 +3601,11 @@ "@types/node": "*" } }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" + }, "node_modules/@types/mime": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", @@ -4090,7 +4239,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -4812,7 +4960,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -5195,8 +5342,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -5247,7 +5393,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, "engines": { "node": ">=6" } @@ -6062,7 +6207,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -6492,7 +6636,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -7499,6 +7642,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", @@ -7532,6 +7680,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8470,6 +8623,34 @@ "node": ">= 6" } }, + "node_modules/protobufjs": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.3.tgz", + "integrity": "sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/protobufjs/node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -8646,7 +8827,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -9197,7 +9377,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -9211,7 +9390,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -10169,7 +10347,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -10232,7 +10409,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -10253,10 +10429,9 @@ } }, "node_modules/yargs": { - "version": "17.7.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", - "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", - "dev": true, + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -10274,7 +10449,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } diff --git a/api/package.json b/api/package.json index 0116d14..b5f50b4 100644 --- a/api/package.json +++ b/api/package.json @@ -8,6 +8,7 @@ "scripts": { "prebuild": "rimraf dist", "build": "nest build", + "build:api": "nest build api-bundled", "build:webpack": "rimraf dist && nest build --webpack", "format": "prettier --write \"apps/**/*.ts\" \"libs/**/*.ts\"", "start": "nest start", @@ -23,6 +24,8 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.301.0", + "@grpc/grpc-js": "^1.8.14", + "@grpc/proto-loader": "^0.7.7", "@nestjs/axios": "^2.0.0", "@nestjs/cache-manager": "^1.0.0", "@nestjs/common": "^9.0.0", @@ -30,6 +33,7 @@ "@nestjs/core": "^9.0.0", "@nestjs/jwt": "^10.0.2", "@nestjs/mapped-types": "^1.2.2", + "@nestjs/microservices": "^9.4.1", "@nestjs/mongoose": "^9.2.2", "@nestjs/passport": "^9.0.3", "@nestjs/platform-express": "^9.0.0",