From 785681451fa00f75ae340d7dcf3c341155e981cf Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 06:36:34 +0100 Subject: [PATCH 01/26] refactor(json-api-nestjs): Remove old files preparation for adaptation for MikroOrm #97 --- libs/json-api/json-api-nestjs/CHANGELOG.md | 67 -- libs/json-api/json-api-nestjs/src/index.ts | 19 - .../src/lib/config/bindings.spec.ts | 9 - .../src/lib/config/bindings.ts | 202 ----- .../src/lib/constants/defaults.ts | 23 - .../src/lib/constants/index.ts | 3 - .../src/lib/constants/postfix.ts | 7 - .../src/lib/constants/reflection.ts | 24 - .../src/lib/decorators/index.ts | 2 - .../inject-service.decorator.spec.ts | 31 - .../inject-service.decorator.ts | 7 - .../json-api/json-api.decorator.spec.ts | 68 -- .../decorators/json-api/json-api.decorator.ts | 19 - .../src/lib/factory/data-source.factory.ts | 14 - .../lib/factory/entity-repository.factory.ts | 25 - .../json-api-nestjs/src/lib/factory/index.ts | 5 - .../src/lib/factory/swagger-bind-method.ts | 8 - .../lib/factory/typeorm-service.factory.ts | 88 -- .../src/lib/factory/zod-validate.factory.ts | 110 --- .../src/lib/helper/bind-controller.spec.ts | 163 ---- .../src/lib/helper/bind-controller.ts | 131 --- .../src/lib/helper/create-controller.spec.ts | 95 --- .../src/lib/helper/create-controller.ts | 51 -- .../src/lib/helper/error-database/index.ts | 89 -- .../lib/helper/error-database/utils.spec.ts | 28 - .../src/lib/helper/error-database/utils.ts | 14 - .../json-api-nestjs/src/lib/helper/index.ts | 7 - .../src/lib/helper/orm/index.ts | 3 - .../orm/methods/delete-one/delete-one.spec.ts | 66 -- .../orm/methods/delete-one/delete-one.ts | 20 - .../delete-relationship.spec.ts | 187 ----- .../delete-relationship.ts | 33 - .../orm/methods/get-all/get-all.spec.ts | 400 --------- .../lib/helper/orm/methods/get-all/get-all.ts | 263 ------ .../orm/methods/get-one/get-one.spec.ts | 187 ----- .../lib/helper/orm/methods/get-one/get-one.ts | 99 --- .../get-relationship/get-relationship.spec.ts | 129 --- .../get-relationship/get-relationship.ts | 71 -- .../src/lib/helper/orm/methods/index.ts | 52 -- .../orm/methods/patch-one/patch-one.spec.ts | 279 ------- .../helper/orm/methods/patch-one/patch-one.ts | 76 -- .../patch-relationship.spec.ts | 228 ------ .../patch-relationship/patch-relationship.ts | 50 -- .../orm/methods/post-one/post-one.spec.ts | 245 ------ .../helper/orm/methods/post-one/post-one.ts | 41 - .../post-relationship.spec.ts | 203 ----- .../post-relationship/post-relationship.ts | 40 - .../src/lib/helper/orm/orm-helper.spec.ts | 273 ------- .../src/lib/helper/orm/orm-helper.ts | 398 --------- .../src/lib/helper/orm/orm-type-asserts.ts | 27 - .../helper/swagger/filter-operand-model.ts | 84 -- .../src/lib/helper/swagger/index.ts | 54 -- .../lib/helper/swagger/method/delete-one.ts | 48 -- .../swagger/method/delete-relationship.ts | 98 --- .../src/lib/helper/swagger/method/get-all.ts | 232 ------ .../src/lib/helper/swagger/method/get-one.ts | 126 --- .../helper/swagger/method/get-relationship.ts | 67 -- .../src/lib/helper/swagger/method/index.ts | 55 -- .../lib/helper/swagger/method/patch-one.ts | 80 -- .../swagger/method/patch-relationship.ts | 95 --- .../src/lib/helper/swagger/method/post-one.ts | 76 -- .../swagger/method/post-relationship.ts | 96 --- .../src/lib/helper/swagger/utils.ts | 306 ------- .../src/lib/helper/utils.spec.ts | 17 - .../json-api-nestjs/src/lib/helper/utils.ts | 56 -- .../src/lib/helper/zod/index.ts | 1 - .../src/lib/helper/zod/zod-helper.spec.ts | 728 ----------------- .../src/lib/helper/zod/zod-helper.ts | 298 ------- .../index.spec.ts | 46 -- .../index.ts | 11 - .../zod/zod-input-patch-schema/index.ts | 27 - .../relationships.spec.ts | 191 ----- .../zod-input-patch-schema/relationships.ts | 64 -- .../index.spec.ts | 43 - .../index.ts | 23 - .../zod-input-post-schema/attributes.spec.ts | 93 --- .../zod/zod-input-post-schema/attributes.ts | 178 ---- .../zod/zod-input-post-schema/data.spec.ts | 37 - .../helper/zod/zod-input-post-schema/data.ts | 24 - .../zod/zod-input-post-schema/id.spec.ts | 36 - .../helper/zod/zod-input-post-schema/id.ts | 14 - .../helper/zod/zod-input-post-schema/index.ts | 22 - .../relationships.spec.ts | 184 ----- .../zod-input-post-schema/relationships.ts | 67 -- .../zod/zod-input-post-schema/type.spec.ts | 21 - .../helper/zod/zod-input-post-schema/type.ts | 4 - .../zod/zod-input-query-schema/filter.spec.ts | 131 --- .../zod/zod-input-query-schema/filter.ts | 135 --- .../zod/zod-input-query-schema/include.ts | 9 - .../zod/zod-input-query-schema/index.ts | 32 - .../helper/zod/zod-input-query-schema/page.ts | 4 - .../zod/zod-input-query-schema/select.spec.ts | 55 -- .../zod/zod-input-query-schema/select.ts | 63 -- .../helper/zod/zod-input-query-schema/sort.ts | 4 - .../zod/zod-query-schema/filter.spec.ts | 483 ----------- .../lib/helper/zod/zod-query-schema/filter.ts | 278 ------- .../zod/zod-query-schema/include.spec.ts | 40 - .../helper/zod/zod-query-schema/include.ts | 16 - .../lib/helper/zod/zod-query-schema/index.ts | 35 - .../helper/zod/zod-query-schema/page.spec.ts | 64 -- .../lib/helper/zod/zod-query-schema/page.ts | 38 - .../zod/zod-query-schema/select.spec.ts | 141 ---- .../lib/helper/zod/zod-query-schema/select.ts | 78 -- .../helper/zod/zod-query-schema/sort.spec.ts | 197 ----- .../lib/helper/zod/zod-query-schema/sort.ts | 94 --- .../src/lib/helper/zod/zod-utils.spec.ts | 108 --- .../src/lib/helper/zod/zod-utils.ts | 56 -- .../src/lib/json-api-nestjs-common.module.ts | 49 -- .../src/lib/json-api.module.ts | 73 -- .../mixin/controller/json-base.controller.ts | 77 -- .../json-api-nestjs/src/lib/mixin/index.ts | 2 - .../mixin/interceptors/error.interceptors.ts | 116 --- .../src/lib/mixin/interceptors/index.ts | 2 - .../interceptors/log-time.interceptors.ts | 25 - .../src/lib/mixin/module/module.mixin.ts | 74 -- .../check-item-entity.pipe.spec.ts | 68 -- .../check-item-entity.pipe.ts | 37 - .../lib/mixin/pipe/check-item-entity/index.ts | 1 - .../src/lib/mixin/pipe/index.ts | 102 --- .../pipe/parse-relationship-name/index.ts | 1 - .../parse-relationship-name.pipe.spec.ts | 62 -- .../parse-relationship-name.pipe.ts | 30 - .../src/lib/mixin/pipe/patch-input/index.ts | 1 - .../pipe/patch-input/patch-input.pipe.spec.ts | 90 -- .../pipe/patch-input/patch-input.pipe.ts | 32 - .../mixin/pipe/patch-relationship/index.ts | 1 - .../patch-relationship.pipe.spec.ts | 98 --- .../patch-relationship.pipe.ts | 35 - .../src/lib/mixin/pipe/post-input/index.ts | 1 - .../pipe/post-input/post-input.pipe.spec.ts | 89 -- .../mixin/pipe/post-input/post-input.pipe.ts | 31 - .../lib/mixin/pipe/post-relationship/index.ts | 1 - .../post-relationship.pipe.spec.ts | 98 --- .../post-relationship.pipe.ts | 35 - .../pipe/query-check-select-field/index.ts | 1 - .../query-check-select-field.spec.ts | 72 -- .../query-check-select-field.ts | 21 - .../pipe/query-filed-on-include/index.ts | 1 - .../query-filed-in-include.pipe.spec.ts | 120 --- .../query-filed-in-include.pipe.ts | 67 -- .../src/lib/mixin/pipe/query-input/index.ts | 1 - .../pipe/query-input/query-input.pipe.spec.ts | 125 --- .../pipe/query-input/query-input.pipe.ts | 32 - .../src/lib/mixin/pipe/query/index.ts | 1 - .../lib/mixin/pipe/query/query.pipe.spec.ts | 115 --- .../src/lib/mixin/pipe/query/query.pipe.ts | 43 - .../src/lib/mixin/service/index.ts | 2 - .../lib/mixin/service/swagger-bind.service.ts | 93 --- .../service/transform-data.service.spec.ts | 375 --------- .../mixin/service/transform-data.service.ts | 258 ------ .../service/typeorm-utils.service.spec.ts | 768 ------------------ .../mixin/service/typeorm-utils.service.ts | 678 ---------------- .../src/lib/mock-utils/db-for-test | 647 --------------- .../src/lib/mock-utils/entities/addresses.ts | 69 -- .../src/lib/mock-utils/entities/comments.ts | 57 -- .../src/lib/mock-utils/entities/index.ts | 29 - .../src/lib/mock-utils/entities/notes.ts | 44 - .../src/lib/mock-utils/entities/pods.ts | 45 - .../entities/requests-have-pod-locks.ts | 91 --- .../src/lib/mock-utils/entities/requests.ts | 48 -- .../src/lib/mock-utils/entities/roles.ts | 58 -- .../lib/mock-utils/entities/user-groups.ts | 20 - .../src/lib/mock-utils/entities/users.ts | 133 --- .../src/lib/mock-utils/index.ts | 94 --- .../src/lib/mock-utils/utils/index.ts | 2 - .../lib/mock-utils/utils/provider-entities.ts | 69 -- .../src/lib/mock-utils/utils/pull-data.ts | 122 --- .../atomic-operation.module.ts | 71 -- .../atomic-operation/constants/index.ts | 10 - .../atomic-operation/controllers/index.ts | 1 - .../controllers/operation.controller.spec.ts | 213 ----- .../controllers/operation.controller.ts | 104 --- .../factory/async-iterator.ts | 75 -- .../modules/atomic-operation/factory/index.ts | 4 - .../factory/map-controller-entity.ts | 25 - .../factory/map-entity-name-to-entity.ts | 17 - .../factory/zod-input-operation.ts | 30 - .../pipes/input-operation.pipe.spec.ts | 75 -- .../pipes/input-operation.pipe.ts | 31 - .../service/execute.service.spec.ts | 478 ----------- .../service/execute.service.ts | 345 -------- .../service/explorer.service.spec.ts | 97 --- .../service/explorer.service.ts | 108 --- .../modules/atomic-operation/service/index.ts | 3 - .../service/swagger.service.ts | 97 --- .../modules/atomic-operation/types/index.ts | 28 - .../modules/atomic-operation/utils/index.ts | 1 - .../utils/zod/zod-helper.spec.ts | 575 ------------- .../atomic-operation/utils/zod/zod-helper.ts | 225 ----- .../service/entity-props-map.service.spec.ts | 85 -- .../lib/service/entity-props-map.service.ts | 96 --- .../json-api-nestjs/src/lib/service/index.ts | 3 - .../service/transform-input.service.spec.ts | 201 ----- .../lib/service/transform-input.service.ts | 149 ---- .../src/lib/types/binding.types.ts | 46 -- .../src/lib/types/decorator-options.types.ts | 8 - .../src/lib/types/error.types.ts | 18 - .../json-api-nestjs/src/lib/types/index.ts | 8 - .../src/lib/types/module.types.ts | 49 -- .../json-api-nestjs/src/lib/types/operand.ts | 27 - .../json-api-nestjs/src/lib/types/response.ts | 13 - .../src/lib/types/typeorm-service.type.ts | 15 - .../json-api-nestjs/src/lib/types/utils.ts | 64 -- .../json-api-nestjs/tsconfig-mjs.lib.json | 13 - libs/json-api/json-shared-type/.eslintrc.json | 18 - libs/json-api/json-shared-type/README.md | 7 - libs/json-api/json-shared-type/jest.config.ts | 11 - libs/json-api/json-shared-type/project.json | 20 - libs/json-api/json-shared-type/src/index.ts | 1 - .../json-shared-type/src/types/entity-type.ts | 17 - .../json-shared-type/src/types/index.ts | 4 - .../json-shared-type/src/types/query-type.ts | 21 - .../src/types/response-body.ts | 69 -- .../json-shared-type/src/types/utils-type.ts | 3 - libs/json-api/json-shared-type/tsconfig.json | 22 - .../json-shared-type/tsconfig.lib.json | 10 - .../json-shared-type/tsconfig.spec.json | 14 - libs/shared-utils/.eslintrc.json | 18 - libs/shared-utils/README.md | 7 - libs/shared-utils/jest.config.ts | 11 - libs/shared-utils/project.json | 27 - libs/shared-utils/src/index.ts | 2 - libs/shared-utils/src/lib/types/index.ts | 1 - .../src/lib/types/utils-string.type.ts | 16 - libs/shared-utils/src/lib/utils/index.ts | 2 - .../src/lib/utils/object-utils.ts | 14 - .../src/lib/utils/string-utils.spec.ts | 37 - .../src/lib/utils/string-utils.ts | 38 - libs/shared-utils/tsconfig.json | 22 - libs/shared-utils/tsconfig.lib.json | 10 - libs/shared-utils/tsconfig.spec.json | 14 - 231 files changed, 19718 deletions(-) delete mode 100644 libs/json-api/json-api-nestjs/CHANGELOG.md delete mode 100644 libs/json-api/json-api-nestjs/src/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/config/bindings.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/utils.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/json-api.module.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/error.types.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/module.types.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/operand.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/response.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/utils.ts delete mode 100644 libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json delete mode 100644 libs/json-api/json-shared-type/.eslintrc.json delete mode 100644 libs/json-api/json-shared-type/README.md delete mode 100644 libs/json-api/json-shared-type/jest.config.ts delete mode 100644 libs/json-api/json-shared-type/project.json delete mode 100644 libs/json-api/json-shared-type/src/index.ts delete mode 100644 libs/json-api/json-shared-type/src/types/entity-type.ts delete mode 100644 libs/json-api/json-shared-type/src/types/index.ts delete mode 100644 libs/json-api/json-shared-type/src/types/query-type.ts delete mode 100644 libs/json-api/json-shared-type/src/types/response-body.ts delete mode 100644 libs/json-api/json-shared-type/src/types/utils-type.ts delete mode 100644 libs/json-api/json-shared-type/tsconfig.json delete mode 100644 libs/json-api/json-shared-type/tsconfig.lib.json delete mode 100644 libs/json-api/json-shared-type/tsconfig.spec.json delete mode 100644 libs/shared-utils/.eslintrc.json delete mode 100644 libs/shared-utils/README.md delete mode 100644 libs/shared-utils/jest.config.ts delete mode 100644 libs/shared-utils/project.json delete mode 100644 libs/shared-utils/src/index.ts delete mode 100644 libs/shared-utils/src/lib/types/index.ts delete mode 100644 libs/shared-utils/src/lib/types/utils-string.type.ts delete mode 100644 libs/shared-utils/src/lib/utils/index.ts delete mode 100644 libs/shared-utils/src/lib/utils/object-utils.ts delete mode 100644 libs/shared-utils/src/lib/utils/string-utils.spec.ts delete mode 100644 libs/shared-utils/src/lib/utils/string-utils.ts delete mode 100644 libs/shared-utils/tsconfig.json delete mode 100644 libs/shared-utils/tsconfig.lib.json delete mode 100644 libs/shared-utils/tsconfig.spec.json diff --git a/libs/json-api/json-api-nestjs/CHANGELOG.md b/libs/json-api/json-api-nestjs/CHANGELOG.md deleted file mode 100644 index 4d2c425f..00000000 --- a/libs/json-api/json-api-nestjs/CHANGELOG.md +++ /dev/null @@ -1,67 +0,0 @@ -## 7.0.4 (2024-10-27) - - -### 🩹 Fixes - -- **json-api-nestjs:** Add filter by null ([3af99ff](https://github.com/klerick/nestjs-json-api/commit/3af99ff)) - - -### ❤️ Thank You - -- Alex H - -## 7.0.3 (2024-05-15) - - -### 🩹 Fixes - -- **json-api-nestjs:** Resource Relationship not allowing data key. ([f648422](https://github.com/klerick/nestjs-json-api/commit/f648422)) - - -### ❤️ Thank You - -- Alex H - -## 7.0.2 (2024-04-21) - - -### 🩹 Fixes - -- **json-api-nestjs:** Fix validation ([e5e9936](https://github.com/klerick/nestjs-json-api/commit/e5e9936)) - -- **json-api-nestjs:** Fix validate for patch method ([40b0303](https://github.com/klerick/nestjs-json-api/commit/40b0303)) - -- **json-api-nestjs:** Fix validate for patch method ([2caa2d8](https://github.com/klerick/nestjs-json-api/commit/2caa2d8)) - - -### ❤️ Thank You - -- Alex H - -## 7.0.1 (2024-04-06) - - -### 🩹 Fixes - -- **json-api-nestjs:** Fix validation ([1d048a8](https://github.com/klerick/nestjs-json-api/commit/1d048a8)) - - -### ❤️ Thank You - -- Alex H - -# 7.0.0 (2024-03-08) - - -### 🚀 Features - -- ⚠️ **json-api-nestjs:** new version json-api ([66076a3](https://github.com/klerick/nestjs-json-api/commit/66076a3)) - - -#### ⚠️ Breaking Changes - -- **json-api-nestjs:** - remove ajv and use zod for validation - -### ❤️ Thank You - -- Alex H \ No newline at end of file diff --git a/libs/json-api/json-api-nestjs/src/index.ts b/libs/json-api/json-api-nestjs/src/index.ts deleted file mode 100644 index 1a4e1274..00000000 --- a/libs/json-api/json-api-nestjs/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { JsonApiModule } from './lib/json-api.module'; -export { InjectService, JsonApi } from './lib/decorators'; -export { - EntityRelation, - TypeormService as JsonApiService, - ResourceObject, - ResourceObjectRelationships, -} from './lib/types'; -export { JsonBaseController } from './lib/mixin/controller/json-base.controller'; -export { - Query, - PatchData, - PostData, - PostRelationshipData, - PatchRelationshipData, - QueryField, -} from './lib/helper/zod'; -export { excludeMethod } from './lib/config/bindings'; -export { entityForClass } from './lib/helper/utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts b/libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts deleted file mode 100644 index 4c256991..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Bindings, excludeMethod } from './bindings'; - -describe('bindings', () => { - it('excludeMethod', () => { - expect(excludeMethod(['patchRelationship'])).toEqual( - Object.keys(Bindings).filter((i) => i !== 'patchRelationship') - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/config/bindings.ts b/libs/json-api/json-api-nestjs/src/lib/config/bindings.ts deleted file mode 100644 index 282f0346..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/config/bindings.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { Body, Param, Query, RequestMethod } from '@nestjs/common'; - -import { BindingsConfig, MethodName } from '../types'; -import { JsonBaseController } from '../mixin'; - -import { PARAMS_RELATION_NAME, PARAMS_RESOURCE_ID } from '../constants'; -import { ObjectTyped } from '../helper'; -import { - queryInputMixin, - queryMixin, - queryFiledInIncludeMixin, - queryCheckSelectFieldMixin, - idPipeMixin, - checkItemEntityPipeMixin, - postInputPipeMixin, - patchInputPipeMixin, - parseRelationshipNamePipeMixin, - postRelationshipPipeMixin, - patchRelationshipPipeMixin, -} from '../mixin/pipe'; - -const Bindings: BindingsConfig = { - getAll: { - method: RequestMethod.GET, - name: 'getAll', - path: '/', - implementation: JsonBaseController.prototype.getAll, - parameters: [ - { - decorator: Query, - mixins: [ - queryInputMixin, - queryMixin, - queryFiledInIncludeMixin, - queryCheckSelectFieldMixin, - ], - }, - ], - }, - getOne: { - method: RequestMethod.GET, - name: 'getOne', - path: `:${PARAMS_RESOURCE_ID}`, - implementation: JsonBaseController.prototype.getOne, - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - decorator: Query, - mixins: [ - queryInputMixin, - queryMixin, - queryFiledInIncludeMixin, - queryCheckSelectFieldMixin, - ], - }, - ], - }, - deleteOne: { - method: RequestMethod.DELETE, - name: 'deleteOne', - path: `:${PARAMS_RESOURCE_ID}`, - implementation: JsonBaseController.prototype.deleteOne, - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - ], - }, - postOne: { - method: RequestMethod.POST, - name: 'postOne', - path: '/', - implementation: JsonBaseController.prototype.postOne, - parameters: [ - { - decorator: Body, - mixins: [postInputPipeMixin], - }, - ], - }, - patchOne: { - method: RequestMethod.PATCH, - name: 'patchOne', - path: `:${PARAMS_RESOURCE_ID}`, - implementation: JsonBaseController.prototype.patchOne, - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - decorator: Body, - mixins: [patchInputPipeMixin], - }, - ], - }, - getRelationship: { - path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, - name: 'getRelationship', - method: RequestMethod.GET, - implementation: JsonBaseController.prototype.getRelationship, - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - property: PARAMS_RELATION_NAME, - decorator: Param, - mixins: [parseRelationshipNamePipeMixin], - }, - ], - }, - postRelationship: { - path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, - name: 'postRelationship', - method: RequestMethod.POST, - implementation: JsonBaseController.prototype['postRelationship'], - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - property: PARAMS_RELATION_NAME, - decorator: Param, - mixins: [parseRelationshipNamePipeMixin], - }, - { - decorator: Body, - mixins: [postRelationshipPipeMixin], - }, - ], - }, - deleteRelationship: { - path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, - name: 'deleteRelationship', - method: RequestMethod.DELETE, - implementation: JsonBaseController.prototype['deleteRelationship'], - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - property: PARAMS_RELATION_NAME, - decorator: Param, - mixins: [parseRelationshipNamePipeMixin], - }, - { - decorator: Body, - mixins: [postRelationshipPipeMixin], - }, - ], - }, - patchRelationship: { - path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, - name: 'patchRelationship', - method: RequestMethod.PATCH, - implementation: JsonBaseController.prototype['patchRelationship'], - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - property: PARAMS_RELATION_NAME, - decorator: Param, - mixins: [parseRelationshipNamePipeMixin], - }, - { - decorator: Body, - mixins: [patchRelationshipPipeMixin], - }, - ], - }, -}; - -export { Bindings }; - -export function excludeMethod( - names: Array> -): Array { - const tmpObject = names.reduce( - (acum, key) => ((acum[key] = true), acum), - {} as Record, boolean> - ); - return ObjectTyped.keys(Bindings).filter( - (method) => !tmpObject[method] - ) as Array; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts b/libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts deleted file mode 100644 index 627c7834..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ParseIntPipe } from '@nestjs/common'; -import { ConfigParam } from '../types'; - -export const DESC = 'DESC'; -export const ASC = 'ASC'; - -export const SORT_TYPE = [DESC, ASC] as const; - -export const DEFAULT_QUERY_PAGE = 1; -export const DEFAULT_PAGE_SIZE = 20; -export const DEFAULT_CONNECTION_NAME = 'default'; - -export const TYPEORM_SERVICE_PROPS = Symbol('typeormService'); - -export const SUB_QUERY_ALIAS_FOR_PAGINATION = 'subQueryWithLimitOffset'; -export const ALIAS_FOR_PAGINATION = 'aliasForPagination'; - -export const ConfigParamDefault: ConfigParam = { - debug: true, - requiredSelectField: true, - pipeForId: ParseIntPipe, - useSoftDelete: false, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/constants/index.ts deleted file mode 100644 index 40c2a13a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './defaults'; -export * from './reflection'; -export * from './postfix'; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts b/libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts deleted file mode 100644 index 0086dee1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const JSON_API_SERVICE_POSTFIX = 'JsonApiService'; -export const CONFIG_PARAM_POSTFIX = 'JsonApiConfigParam'; -export const JSON_API_CONTROLLER_POSTFIX = 'JsonApiController'; -export const JSON_API_MODULE_POSTFIX = 'JsonApiModule'; -export const TYPEORM_UTILS_SERVICE_POSTFIX = 'TypeormUtilsService'; -export const TYPEORM_MIXIN_SERVICE_POSTFIX = 'JsonApiTypeormService'; -export const TRANSFORM_MIXIN_SERVICE_POSTFIX = 'JsonApiTransformService'; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts b/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts deleted file mode 100644 index 06af80da..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const JSON_API_DECORATOR_ENTITY = Symbol('JSON_API_ENTITY'); -export const JSON_API_DECORATOR_OPTIONS = Symbol('JSON_API_OPTIONS'); -export const GLOBAL_MODULE_OPTIONS_TOKEN = Symbol('GLOBAL_MODULE_OPTIONS'); -export const CURRENT_DATA_SOURCE_TOKEN = Symbol('CURRENT_DATA_SOURCE_TOKEN'); -export const CURRENT_ENTITY_REPOSITORY = Symbol('CURRENT_ENTITY_REPOSITORY'); -export const ZOD_INPUT_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); -export const ZOD_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); -export const ZOD_POST_SCHEMA = Symbol('ZOD_POST_SCHEMA'); -export const SWAGGER_METHOD = Symbol('SWAGGER_METHOD'); -export const ZOD_POST_RELATIONSHIP_SCHEMA = Symbol( - 'ZOD_POST_RELATIONSHIP_SCHEMA' -); -export const ZOD_PATCH_RELATIONSHIP_SCHEMA = Symbol( - 'ZOD_PATCH_RELATIONSHIP_SCHEMA' -); -export const ZOD_PATCH_SCHEMA = Symbol('ZOD_PATCH_SCHEMA'); -export const TYPEORM_SERVICE = Symbol('TYPEORM_SERVICE'); -export const CONTROL_OPTIONS_TOKEN = Symbol('CONTROL_OPTIONS_TOKEN'); - -export const PARAMS_RESOURCE_ID = 'id'; -export const PARAMS_RELATION_ID = 'relId'; -export const PARAMS_RELATION_NAME = 'relName'; - -export const JSON_API_CONFIG = 'JSON_API_CONFIG'; diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/index.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/index.ts deleted file mode 100644 index 2036a50b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './json-api/json-api.decorator'; -export * from './inject-service/inject-service.decorator'; diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts deleted file mode 100644 index cf68012c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - PROPERTY_DEPS_METADATA, - SELF_DECLARED_DEPS_METADATA, -} from '@nestjs/common/constants'; -import { getProviderName } from '../../helper'; -import 'reflect-metadata'; - -import { InjectService } from './inject-service.decorator'; -import { TYPEORM_SERVICE } from '../../constants'; - -describe('InjectServiceDecorator', () => { - it('should save property key', () => { - class SomeClass { - @InjectService() protected property: any; - constructor(@InjectService() protected test: any) {} - } - - const properties = Reflect.getMetadata(PROPERTY_DEPS_METADATA, SomeClass); - const properties1 = Reflect.getMetadata( - SELF_DECLARED_DEPS_METADATA, - SomeClass - ); - expect( - properties.find((item: any) => item.type === TYPEORM_SERVICE) - ).toBeDefined(); - - expect( - properties1.find((item: any) => item.param === TYPEORM_SERVICE) - ).toBeDefined(); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts deleted file mode 100644 index a51b36a9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Inject } from '@nestjs/common'; - -import { TYPEORM_SERVICE } from '../../constants'; - -export function InjectService(): PropertyDecorator & ParameterDecorator { - return Inject(TYPEORM_SERVICE); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts deleted file mode 100644 index 6f7ffe86..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import 'reflect-metadata'; - -import { - JSON_API_DECORATOR_ENTITY, - JSON_API_DECORATOR_OPTIONS, -} from '../../constants/reflection'; -import { JsonApi } from './json-api.decorator'; -import { DecoratorOptions } from '../../types'; -import { excludeMethod, Bindings } from '../../config/bindings'; - -describe('InjectServiceDecorator', () => { - it('should save entity in class', () => { - const testedEntity = class SomeEntity {}; - - @JsonApi(testedEntity) - class SomeClass {} - - const data = Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, SomeClass); - expect(data).toBe(testedEntity); - }); - - it('should save options in class', () => { - const testedEntity = class SomeEntity {}; - const apiOptions: DecoratorOptions = { - allowMethod: ['getAll', 'deleteRelationship'], - }; - - @JsonApi(testedEntity, apiOptions) - class SomeClass {} - - const data = Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, SomeClass); - expect(data).toEqual(apiOptions); - }); - - it('should save options in class using helpFunction', () => { - const testedEntity = class SomeEntity {}; - const example = ['getAll', 'deleteRelationship']; - const apiOptions: DecoratorOptions = { - allowMethod: excludeMethod(example as any), - }; - - @JsonApi(testedEntity, apiOptions) - class SomeClass {} - - const data: DecoratorOptions = Reflect.getMetadata( - JSON_API_DECORATOR_OPTIONS, - SomeClass - ); - expect(data).toEqual(apiOptions); - expect(data.allowMethod).toEqual( - Object.keys(Bindings).filter((k) => !example.includes(k)) - ); - }); - - it('should save options in class and correctly set overrideRoute', () => { - const testedEntity = class SomeEntity {}; - const apiOptions: DecoratorOptions = { - allowMethod: ['getAll', 'deleteRelationship'], - overrideRoute: '123' - }; - - @JsonApi(testedEntity, apiOptions) - class SomeClass {} - - const data = Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, SomeClass); - expect(data).toEqual(apiOptions); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts deleted file mode 100644 index f0297ded..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - JSON_API_DECORATOR_ENTITY, - JSON_API_DECORATOR_OPTIONS, -} from '../../constants'; -import { Entity } from '../../types'; -import { DecoratorOptions } from '../../types'; - -export function JsonApi( - entity: Entity, - options?: DecoratorOptions -): ClassDecorator { - return (target): typeof target => { - Reflect.defineMetadata(JSON_API_DECORATOR_ENTITY, entity, target); - if (options) { - Reflect.defineMetadata(JSON_API_DECORATOR_OPTIONS, options, target); - } - return target; - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts deleted file mode 100644 index 4d284072..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { FactoryProvider } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../constants'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -export function CurrentDataSourceProvider( - connectionName?: string -): FactoryProvider { - return { - provide: CURRENT_DATA_SOURCE_TOKEN, - useFactory: (dataSource: DataSource) => dataSource, - inject: [getDataSourceToken(connectionName)], - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts deleted file mode 100644 index 7903f4e6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { FactoryProvider } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; - -import { Entity } from '../types'; -import { - CURRENT_DATA_SOURCE_TOKEN, - CURRENT_ENTITY_REPOSITORY, -} from '../constants'; -import { EntityTarget } from 'typeorm/common/EntityTarget'; - -export function EntityRepositoryFactory( - entity: E -): FactoryProvider> { - return { - provide: CURRENT_ENTITY_REPOSITORY, - useFactory: (dataSource: DataSource) => - dataSource.getRepository(entity as EntityTarget), - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/factory/index.ts deleted file mode 100644 index b529e2bf..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './zod-validate.factory'; -export * from './data-source.factory'; -export * from './typeorm-service.factory'; -export * from './entity-repository.factory'; -export * from './swagger-bind-method'; diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts b/libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts deleted file mode 100644 index 0ab682cc..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ValueProvider } from '@nestjs/common'; -import { SwaggerMethod, swaggerMethod } from '../helper/swagger/method'; -import { SWAGGER_METHOD } from '../constants'; - -export const SwaggerBindMethod: ValueProvider = { - provide: SWAGGER_METHOD, - useValue: swaggerMethod, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts deleted file mode 100644 index 28304051..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { FactoryProvider } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; - -import { - ConfigParam, - Entity, - TypeormService, - TypeormServiceObject, -} from '../types'; -import { - CONFIG_PARAM_POSTFIX, - TYPEORM_UTILS_SERVICE_POSTFIX, - CURRENT_DATA_SOURCE_TOKEN, - TYPEORM_SERVICE, - CURRENT_ENTITY_REPOSITORY, - CONTROL_OPTIONS_TOKEN, -} from '../constants'; - -import { - getEntityName, - getProviderName, - MethodsService, - ObjectTyped, -} from '../helper'; -import { TypeormUtilsService } from '../service'; -import { TransformDataService } from '../mixin/service'; - -function guardMethodsServiceName( - methodsService: R, - key: any -): asserts key is keyof R { - if (!(key in methodsService)) - throw new Error(`${key} is not methode of MethodsService`); -} - -export function TypeormServiceFactory( - entity: E -): FactoryProvider> { - const entityName = getEntityName(entity as any); - return { - provide: TYPEORM_SERVICE, - inject: [ - { - token: CURRENT_ENTITY_REPOSITORY, - optional: false, - }, - { - token: CONTROL_OPTIONS_TOKEN, - optional: false, - }, - TypeormUtilsService, - TransformDataService, - ], - useFactory: ( - repository: Repository, - config: ConfigParam, - typeormUtilsService: TypeormUtilsService, - transformDataService: TransformDataService - ) => { - const typeOrmObject: TypeormServiceObject = { - repository, - config, - typeormUtilsService, - transformDataService, - }; - - const bindMethods = ObjectTyped.entries(MethodsService).reduce( - (acum, [key, val]) => ({ - ...acum, - [key]: (val as any).bind(typeOrmObject) as typeof val, - }), - {} as typeof MethodsService - ); - - const target = {} as TypeormService; - return new Proxy>(target, { - get(target: {}, p: string | symbol, receiver: any): any { - try { - guardMethodsServiceName(bindMethods, p); - return bindMethods[p]; - } catch (e) { - return undefined; - } - }, - }); - }, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts deleted file mode 100644 index 52952be5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Entity, ModuleOptions } from '../types'; -import { FactoryProvider, ValueProvider } from '@nestjs/common'; -import { - ZodInputQuerySchema, - zodInputQuerySchema, - ZodQuerySchema, - zodQuerySchema, - zodInputPostSchema, - ZodInputPostSchema, - zodInputPatchSchema, - ZodInputPatchSchema, - ZodInputPostRelationshipSchema as ZodInputPostRelationshipSchemaType, - ZodInputPatchRelationshipSchema as ZodInputPatchRelationshipSchemaType, - zodInputPostRelationshipSchema, - zodInputPatchRelationshipSchema, -} from '../helper/zod'; -import { - CURRENT_DATA_SOURCE_TOKEN, - ZOD_INPUT_QUERY_SCHEMA, - ZOD_POST_SCHEMA, - ZOD_PATCH_SCHEMA, - ZOD_POST_RELATIONSHIP_SCHEMA, - ZOD_PATCH_RELATIONSHIP_SCHEMA, - ZOD_QUERY_SCHEMA, -} from '../constants'; - -import { EntityTarget } from 'typeorm/common/EntityTarget'; - -export function ZodInputQuerySchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_INPUT_QUERY_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodInputQuerySchema( - dataSource.getRepository(entity as EntityTarget) - ), - }; -} - -export function ZodQuerySchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_QUERY_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodQuerySchema(dataSource.getRepository(entity as EntityTarget)), - }; -} - -export function ZodInputPostSchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_POST_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodInputPostSchema( - dataSource.getRepository(entity as EntityTarget) - ), - }; -} - -export function ZodInputPatchSchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_PATCH_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodInputPatchSchema( - dataSource.getRepository(entity as EntityTarget) - ), - }; -} - -export const ZodInputPostRelationshipSchema: ValueProvider = - { - provide: ZOD_POST_RELATIONSHIP_SCHEMA, - useValue: zodInputPostRelationshipSchema, - }; - -export const ZodInputPatchRelationshipSchema: ValueProvider = - { - provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, - useValue: zodInputPatchRelationshipSchema, - }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts deleted file mode 100644 index 4c35d4a5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { bindController } from './bind-controller'; -import { Users } from '../mock-utils'; -import { DEFAULT_CONNECTION_NAME } from '../constants'; -import { - ParseIntPipe, - Query, - Body, - Param, - PipeTransform, - ArgumentMetadata, -} from '@nestjs/common'; -import { TypeormService } from '../types'; -import { PatchData } from './zod'; -import { JsonApi } from '../decorators'; -import { JsonBaseController } from '../mixin/controller/json-base.controller'; -import { excludeMethod } from '../config/bindings'; -import { - METHOD_METADATA, - PATH_METADATA, - ROUTE_ARGS_METADATA, -} from '@nestjs/common/constants'; -import { ObjectTyped } from './utils'; -import { Bindings } from '../config/bindings'; -import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; - -const mapParams = new Map(); -mapParams.set(Query, RouteParamtypes.QUERY); -mapParams.set(Body, RouteParamtypes.BODY); -mapParams.set(Param, RouteParamtypes.PARAM); - -describe('bindController', () => { - it('Should be all methode', () => { - class Controller {} - const config = { - requiredSelectField: false, - pipeForId: ParseIntPipe, - debug: false, - useSoftDelete: false, - }; - bindController(Controller, Users, DEFAULT_CONNECTION_NAME, config); - - expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ - 'constructor', - 'getAll', - 'getOne', - 'deleteOne', - 'postOne', - 'patchOne', - 'getRelationship', - 'postRelationship', - 'deleteRelationship', - 'patchRelationship', - ]); - - for (const [key, value] of ObjectTyped.entries(Bindings)) { - const descriptor = Reflect.getOwnPropertyDescriptor( - Controller.prototype, - key - ); - if (!descriptor) { - throw new Error('descriptor is empty:' + key); - } - - expect(Reflect.getMetadata(PATH_METADATA, descriptor.value)).toBe( - value.path - ); - expect(Reflect.getMetadata(METHOD_METADATA, descriptor.value)).toBe( - value.method - ); - const paramsMetadata = Reflect.getMetadata( - ROUTE_ARGS_METADATA, - Controller.prototype.constructor, - key - ); - for (const params in value.parameters) { - const tmp = value.parameters[params]; - if (!tmp.decorator) { - expect(paramsMetadata).toEqual(tmp.decorator); - continue; - } - const paramsMetadataItem = - paramsMetadata[`${mapParams.get(tmp.decorator)}:${params}`]; - expect(paramsMetadataItem).not.toEqual(undefined); - expect(paramsMetadataItem.index).toBe(parseInt(params)); - tmp.mixins.forEach((i, k) => { - expect(i(Users, DEFAULT_CONNECTION_NAME, config).name).toEqual( - paramsMetadataItem.pipes[k].name - ); - }); - } - } - }); - - it('Should be without methode: postOne, getRelationship', () => { - @JsonApi(Users, { - allowMethod: excludeMethod(['postOne', 'getRelationship']), - }) - class Controller {} - const config = { - requiredSelectField: false, - pipeForId: ParseIntPipe, - debug: false, - useSoftDelete: false, - }; - bindController(Controller, Users, DEFAULT_CONNECTION_NAME, config); - expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ - 'constructor', - 'getAll', - 'getOne', - 'deleteOne', - 'patchOne', - 'postRelationship', - 'deleteRelationship', - 'patchRelationship', - ]); - }); - - it('Should be use custom pipe', () => { - class SomePipes implements PipeTransform { - transform(value: any, metadata: ArgumentMetadata): any { - return undefined; - } - } - class Controller extends JsonBaseController { - override patchOne( - @Param('id', SomePipes) id: string | number, - @Body(SomePipes) inputData: PatchData - ): ReturnType['patchOne']> { - return super.patchOne(id, inputData); - } - } - const config = { - requiredSelectField: false, - pipeForId: SomePipes, - debug: false, - useSoftDelete: false, - }; - bindController(Controller, Users, DEFAULT_CONNECTION_NAME, config); - - const paramsMetadata = Reflect.getMetadata( - ROUTE_ARGS_METADATA, - Controller.prototype.constructor, - 'patchOne' - ); - expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes[0]).toEqual( - SomePipes - ); - - expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes.length).toBe( - Bindings.patchOne.parameters[0].mixins.length + 1 - ); - expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes.at(-1)).toEqual( - SomePipes - ); - - expect(paramsMetadata[`${mapParams.get(Body)}:1`].pipes.length).toBe( - Bindings.patchOne.parameters[1].mixins.length + 1 - ); - expect(paramsMetadata[`${mapParams.get(Body)}:1`].pipes.at(-1)).toEqual( - SomePipes - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts b/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts deleted file mode 100644 index 28eff2c9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - Body, - Delete, - Get, - HttpCode, - Param, - Patch, - Post, - Query, - RequestMethod, -} from '@nestjs/common'; -import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; -import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; - -import { Bindings } from '../config/bindings'; -import { - ConfigParam, - DecoratorOptions, - Entity, - ExtractNestType, - MethodName, - NestController, -} from '../types'; -import { JSON_API_DECORATOR_OPTIONS } from '../constants'; - -export function bindController( - controller: ExtractNestType, - entity: Entity, - connectionName: string, - config: ConfigParam -): void { - for (const methodName in Bindings) { - const { name, path, parameters, method, implementation } = - Bindings[methodName as MethodName]; - - const decoratorOptions: DecoratorOptions = Reflect.getMetadata( - JSON_API_DECORATOR_OPTIONS, - controller - ); - if (decoratorOptions) { - const { allowMethod = Object.keys(Bindings) } = decoratorOptions; - if (!allowMethod.includes(name)) continue; - } - - if (!Object.prototype.hasOwnProperty.call(controller.prototype, name)) { - // need uniq descriptor for correct work swagger - Reflect.defineProperty(controller.prototype, name, { - value: function ( - ...arg: Parameters - ): ReturnType { - return this.constructor.__proto__.prototype[name].call(this, ...arg); - }, - writable: true, - enumerable: false, - configurable: true, - }); - } - - const descriptor = Reflect.getOwnPropertyDescriptor( - controller.prototype, - name - ); - - if (!descriptor) { - throw new Error( - `Descriptor for "${controller.name}[${name}]" is undefined` - ); - } - - switch (method) { - case RequestMethod.GET: { - Get(path)(controller.prototype, name, descriptor); - break; - } - case RequestMethod.DELETE: { - HttpCode(204)(controller.prototype, name, descriptor); - Delete(path)(controller.prototype, name, descriptor); - break; - } - case RequestMethod.POST: { - Post(path)(controller.prototype, name, descriptor); - break; - } - case RequestMethod.PATCH: { - Patch(path)(controller.prototype, name, descriptor); - break; - } - default: { - throw new Error(`Method '${method}' unsupported`); - } - } - const paramsMetadata = Reflect.getMetadata( - ROUTE_ARGS_METADATA, - controller.prototype.constructor, - name - ); - for (const key in parameters) { - const parameter = parameters[key]; - const { property, decorator, mixins } = parameter; - const resultMixin = mixins.map((mixin) => - mixin(entity, connectionName, config) - ); - - if (paramsMetadata) { - let typeDecorator: RouteParamtypes; - switch (decorator) { - case Query: - typeDecorator = RouteParamtypes.QUERY; - break; - case Param: - typeDecorator = RouteParamtypes.PARAM; - break; - case Body: - typeDecorator = RouteParamtypes.BODY; - } - const tmp = Object.entries(paramsMetadata) - .filter(([k, v]) => k.split(':').at(0) === typeDecorator.toString()) - .reduce( - (acum, [k, v]) => (acum.push(...(v as any).pipes), acum), - [] as any - ); - resultMixin.push(...tmp); - } - decorator(property, ...resultMixin)( - controller.prototype, - name, - parseInt(key, 10) - ); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts deleted file mode 100644 index 2759ce9c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - CONTROLLER_WATERMARK, - INTERCEPTORS_METADATA, - PATH_METADATA, - PROPERTY_DEPS_METADATA, -} from '@nestjs/common/constants'; -import { createController } from './create-controller'; -import { Users } from '../mock-utils'; -import { JsonBaseController } from '../mixin/controller/json-base.controller'; -import { - JSON_API_CONTROLLER_POSTFIX, - TYPEORM_SERVICE, - TYPEORM_SERVICE_PROPS, -} from '../constants'; -import { InjectService, JsonApi } from '../decorators'; -import { ErrorInterceptors, LogTimeInterceptors } from '../mixin/interceptors'; - -describe('createController', () => { - it('Should be error', () => { - class TestController {} - expect.assertions(2); - try { - createController(Users, TestController); - } catch (e) { - expect(e).toBeInstanceOf(Error); - expect((e as Error).message).toBe( - 'Controller "TestController" should be inherited of "JsonBaseController"' - ); - } - }); - - it('Should be correct name controller', () => { - class TestController extends JsonBaseController {} - const result = createController(Users); - const result1 = createController(Users, TestController); - expect(result.name).toBe('Users' + JSON_API_CONTROLLER_POSTFIX); - expect(result1.name).toBe('TestController'); - }); - - it('Should be correct path for controller', () => { - const overrideRoute = 'override-route'; - class TestController extends JsonBaseController {} - - @JsonApi(Users, { - overrideRoute, - }) - class TestController2 extends JsonBaseController {} - const result = createController(Users); - const result2 = createController(Users, TestController); - const result3 = createController(Users, TestController2); - - expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result)).toBe(true); - expect(Reflect.getMetadata(PATH_METADATA, result)).toBe('users'); - - expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result2)).toBe(true); - expect(Reflect.getMetadata(PATH_METADATA, result2)).toBe('users'); - - expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result3)).toBe(true); - expect(Reflect.getMetadata(PATH_METADATA, result3)).toBe(overrideRoute); - }); - - it('Check inject typeorm, service', () => { - class TestController extends JsonBaseController { - @InjectService() private tmp: any; - } - - const result = createController(Users); - const result1 = createController(Users, TestController); - - const check = Reflect.getMetadata( - PROPERTY_DEPS_METADATA, - result.prototype.constructor - ); - const check1 = Reflect.getMetadata( - PROPERTY_DEPS_METADATA, - result1.prototype.constructor - ); - - const intecept = Reflect.getMetadata( - INTERCEPTORS_METADATA, - result1.prototype.constructor - ); - expect(intecept).not.toBe(undefined); - expect(intecept[0]).toEqual(LogTimeInterceptors); - expect(intecept[1]).toEqual(ErrorInterceptors); - expect(check[0].key).toBe(TYPEORM_SERVICE_PROPS); - expect(check[0].type).toEqual(TYPEORM_SERVICE); - - expect(check1[0].key).toBe('tmp'); - expect(check1[0].type).toEqual(TYPEORM_SERVICE); - - expect(check1[1].key).toBe(TYPEORM_SERVICE_PROPS); - expect(check1[1].type).toEqual(TYPEORM_SERVICE); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts b/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts deleted file mode 100644 index d4ca8078..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Controller, Inject, Type, UseInterceptors } from '@nestjs/common'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; - -import { camelToKebab, getProviderName, nameIt } from './utils'; -import { - JSON_API_CONTROLLER_POSTFIX, - JSON_API_DECORATOR_OPTIONS, - TYPEORM_SERVICE, - TYPEORM_SERVICE_PROPS, -} from '../constants'; -import { JsonBaseController } from '../mixin/controller/json-base.controller'; -import { ErrorInterceptors, LogTimeInterceptors } from '../mixin/interceptors'; - -import { DecoratorOptions } from '../types'; - -export function createController( - entity: EntityClassOrSchema, - controller?: Type -): Type { - const controllerClass = - controller || - nameIt( - getProviderName(entity, JSON_API_CONTROLLER_POSTFIX), - JsonBaseController - ); - - const entityName = - entity instanceof Function ? entity.name : entity.options.name; - - if ( - !Object.prototype.isPrototypeOf.call(JsonBaseController, controllerClass) - ) { - throw new Error( - `Controller "${controller?.name}" should be inherited of "JsonBaseController"` - ); - } - - const decoratorOptions: DecoratorOptions = Reflect.getMetadata( - JSON_API_DECORATOR_OPTIONS, - controllerClass - ); - - Controller( - decoratorOptions?.['overrideRoute'] || `${camelToKebab(entityName)}` - )(controllerClass); - - Inject(TYPEORM_SERVICE)(controllerClass.prototype, TYPEORM_SERVICE_PROPS); - UseInterceptors(LogTimeInterceptors)(controllerClass); - UseInterceptors(ErrorInterceptors)(controllerClass); - return controllerClass; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts deleted file mode 100644 index 07ef1b42..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { EntityMetadata } from 'typeorm'; -import { ValidateQueryError } from '../../types'; - -import { formErrorString } from './utils'; -import { - BadRequestException, - ConflictException, - NotAcceptableException, -} from '@nestjs/common'; - -const fieldNotNullOrDefault = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: formErrorString(entityMetadata, errorText), - path: ['data', 'attributes'], - }; - - return new BadRequestException([error]); -}; - -const duplicateItems = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - errorText = 'Duplicate value'; - if (detail) { - const matches = detail.match(/(?<=\().+?(?=\))/gm); - if (matches) { - errorText = `Duplicate value in the "${matches[0]}"`; - } - } - - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: detail ? formErrorString(entityMetadata, errorText) : errorText, - path: ['data', 'attributes'], - }; - - return new ConflictException([error]); -}; - -const invalidInputSyntax = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: errorText, - path: [], - }; - return new BadRequestException([error]); -}; - -const entityHasRelation = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: detail || errorText, - path: ['data', 'attributes'], - }; - return new NotAcceptableException([error]); -}; - -export const MysqlError = { - [1364]: fieldNotNullOrDefault, - [1062]: duplicateItems, - [1525]: invalidInputSyntax, -}; - -export const PostgresError = { - [23502]: fieldNotNullOrDefault, - [23505]: duplicateItems, - ['22P02']: invalidInputSyntax, - [22007]: invalidInputSyntax, - [22003]: invalidInputSyntax, - [23503]: entityHasRelation, -}; - -export type PostgresErrorCode = keyof typeof PostgresError; -export type MysqlErrorCode = keyof typeof MysqlError; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts deleted file mode 100644 index 7fe1e600..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { formErrorString } from './utils'; - -const metadata = { - tableName: 'users', - name: 'Users', - columns: [ - { - databaseName: 'created_at', - propertyName: 'createdAt', - }, - { - databaseName: 'addresses_id', - propertyName: 'addresses', - }, - ], -} as any; - -describe('utils', () => { - it('formErrorString', () => { - const result = formErrorString( - metadata, - `null value in column "${metadata.columns[1].propertyName}" of relation "${metadata.tableName}" violates not-null constraint` - ); - expect(result).toBe( - `null value in column "${metadata.columns[1].propertyName}" of relation "${metadata.name}" violates not-null constraint` - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts deleted file mode 100644 index 06e0f01a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EntityMetadata } from 'typeorm'; - -export const formErrorString = ( - entityMetadata: EntityMetadata, - errorText: string -) => { - for (const column of entityMetadata.columns) { - const result = new RegExp(column.databaseName).test(errorText); - if (!result) continue; - - errorText = errorText.replace(column.databaseName, column.propertyName); - } - return errorText.replace(entityMetadata.tableName, entityMetadata.name); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/index.ts deleted file mode 100644 index de801b8a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './utils'; -export * from './bind-controller'; -export * from './create-controller'; -export * from './zod/zod-helper'; -export * from './swagger'; -export { MethodsService } from './orm'; -export * from './error-database'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts deleted file mode 100644 index b687d4dd..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './orm-helper'; -export * from './orm-type-asserts'; -export * from './methods'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts deleted file mode 100644 index a52e18e5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, -} from '../../../../mock-utils'; -import { CurrentDataSourceProvider } from '../../../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; -import { TypeormService } from '../../../../types'; -import { getRepository, pullUser, Users } from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { Repository } from 'typeorm'; -import { CONTROL_OPTIONS_TOKEN, TYPEORM_SERVICE } from '../../../../constants'; -import { - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { EntityPropsMapService } from '../../../../service'; - -describe('deleteOne', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let user: Users; - let userRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ userRepository } = getRepository(module)); - user = await pullUser(userRepository); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - }); - - it('Should be ok', async () => { - await typeormService.deleteOne(`${user.id}`); - expect(await userRepository.findOneBy({ id: user.id })).toBe(null); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts deleted file mode 100644 index 573a7ed4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Entity, TypeormServiceObject } from '../../../../types'; -import { FindOptionsWhere } from 'typeorm'; - -export async function deleteOne( - this: TypeormServiceObject, - id: number | string -): Promise { - const data = await this.repository.findOne({ - where: { - [this.typeormUtilsService.currentPrimaryColumn.toString()]: id, - } as FindOptionsWhere, - }); - if (!data) return void 0; - - this.config.useSoftDelete - ? await this.repository.softRemove(data) - : await this.repository.remove(data); - - return void 0; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts deleted file mode 100644 index 8c45cdc0..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Repository } from 'typeorm'; - -import { TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { EntityPropsMapService } from '../../../../service'; - -describe('deleteRelationship', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - let typeormUtilsService: TypeormUtilsService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - typeormUtilsService = - module.get>(TypeormUtilsService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', async () => { - const checkUser = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - - const roles = await rolesRepository.find(); - const userGroups = await userGroupRepository.find(); - const users = await userRepository.find(); - - if (!checkUser) { - throw new Error('not found mock'); - } - - const userGroupData = { - type: 'user-groups', - id: userGroups - .find((i) => checkUser.userGroup.id === i.id) - ?.id.toString(), - }; - const rolesData = [ - { - type: 'roles', - id: roles - .find((i) => checkUser.roles.find((a) => a.id === i.id)) - ?.id.toString(), - }, - ]; - - const managerData = { - type: 'users', - id: users.find((i) => checkUser.manager.id === i.id)?.id.toString(), - }; - await typeormService.deleteRelationship(1, 'roles', rolesData as any); - await typeormService.deleteRelationship( - 1, - 'userGroup', - userGroupData as any - ); - await typeormService.deleteRelationship(1, 'manager', managerData as any); - - const checkUserAfterPost = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - if (!checkUserAfterPost) { - throw new Error('not found'); - } - expect(checkUserAfterPost.manager).toBe(null); - expect(checkUserAfterPost.roles.map((i) => i.id.toString()).sort()).toEqual( - checkUser.roles - .map((i) => i.id.toString()) - .filter((i) => !rolesData.map((i) => i.id).includes(i)) - .sort() - ); - expect(checkUserAfterPost.userGroup).toBe(null); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts deleted file mode 100644 index 427ed6cb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - Entity, - TypeormServiceObject, - EntityRelation, -} from '../../../../types'; - -import { PostRelationshipData } from '../../../zod'; - -export async function deleteRelationship< - E extends Entity, - Rel extends EntityRelation ->( - this: TypeormServiceObject, - id: number | string, - rel: Rel, - input: PostRelationshipData -): Promise { - const idsResult = await this.typeormUtilsService.validateRelationInputData( - rel, - input - ); - const postBuilder = this.repository - .createQueryBuilder() - .relation(rel.toString()) - .of(id); - - if (Array.isArray(idsResult)) { - await postBuilder.remove(idsResult); - } else { - await postBuilder.set(null); - } - return void 0; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts deleted file mode 100644 index 953d8b91..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, - DEFAULT_QUERY_PAGE, - DEFAULT_PAGE_SIZE, -} from '../../../../constants'; -import { Entity, TypeormService } from '../../../../types'; -import { Query, QueryField } from '../../../../helper'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { Equal, IsNull, Repository } from 'typeorm'; -import { EntityPropsMapService } from '../../../../service'; - -function getDefaultQuery() { - const filter = { - relation: null, - target: null, - }; - const defaultQuery: Query = { - [QueryField.filter]: filter, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }; - - return defaultQuery; -} - -describe('getAll', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('order', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - - const checkData = await userRepository.find({ - relations: { - addresses: true, - comments: true, - }, - order: { - id: 'DESC', - comments: { - id: 'DESC', - }, - }, - }); - - const query = getDefaultQuery(); - query.include = ['addresses', 'comments']; - query.sort = { - target: { - id: 'DESC', - }, - comments: { - id: 'DESC', - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith(checkData); - }); - - it('include', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - - const checkData = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - addresses: true, - comments: true, - }, - }); - - const query = getDefaultQuery(); - query.include = ['addresses', 'comments']; - query.filter.target = { - id: { - eq: `${checkData?.id}`, - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - it('select', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - - const checkData = await userRepository.findOne({ - select: { - id: true, - isActive: true, - addresses: { - state: true, - id: true, - }, - comments: { - text: true, - id: true, - }, - }, - where: { - id: 1, - }, - relations: { - addresses: true, - comments: true, - }, - }); - - const query = getDefaultQuery(); - query.fields = { - target: ['id', 'isActive'], - addresses: ['state'], - comments: ['text'], - }; - query.include = ['addresses', 'comments']; - query.filter.target = { - id: { - eq: `${checkData?.id}`, - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - describe('filter', () => { - let firstRole: Roles; - let secondRole: Roles; - let addresses: Addresses[]; - let comments: Comments[]; - beforeAll(async () => { - firstRole = (await rolesRepository.findOneBy({ - id: 1, - })) as Roles; - secondRole = (await rolesRepository.findOneBy({ - id: 2, - })) as Roles; - - addresses = await addressesRepository.find(); - comments = await commentsRepository.find(); - }); - - it('Target props with null', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - - const query = getDefaultQuery(); - query.filter.target = { - id: { eq: '1' }, - firstName: {eq: null}, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toHaveBeenCalledTimes(0); - }); - - it('Target props', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.findOne({ - where: { - id: 1, - }, - }); - const query = getDefaultQuery(); - query.filter.target = { - id: { eq: `${checkData?.id}` }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - it('Check relation with the same Entity', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.findOne({ - where: { - id: 1, - comments: { - text: Equal(comments[0].text), - }, - }, - relations: { - comments: true, - }, - }); - const query = getDefaultQuery(); - query.filter.relation = { - comments: { - text: { - eq: comments[0].text, - }, - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - // it('Target relation is null', async () => { - // const query = getDefaultQuery(); - // query.filter.target = { - // comments: { - // eq: 'null', - // }, - // }; - // await typeormService.getAll(query); - // }); - - it('Relation many-to-one', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - manager: true, - }, - }); - - const query = getDefaultQuery(); - query.filter.target = { - id: { - eq: '1', - }, - }; - query.filter.relation = { - manager: { - id: { - eq: '2', - }, - }, - }; - query.include = ['manager']; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - it('Relation one-to-many', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.findOne({ - where: { - id: 1, - addresses: { - state: Equal(addresses[0].state), - }, - }, - relations: { - addresses: true, - }, - }); - const query = getDefaultQuery(); - query.filter.relation = { - addresses: { - state: { - eq: addresses[0].state, - }, - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - it('Relation many-to-many', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.find({ - where: { - id: 1, - roles: { - name: Equal(firstRole.name), - }, - }, - relations: { - roles: true, - }, - }); - - const query = getDefaultQuery(); - query.include = ['roles']; - query.filter.relation = { - roles: { - name: { - eq: firstRole.name, - }, - }, - }; - const { data } = await typeormService.getAll(query); - expect(spyOnTransformData).not.toBeCalled(); - expect(data).toEqual([]); - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts deleted file mode 100644 index e0e60fb9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { Entity, TypeormServiceObject } from '../../../../types'; -import { Query } from '../../../zod'; -import { ObjectTyped } from '../../../utils'; -import { TupleOfEntityRelation } from '../../orm-helper'; -import { - ALIAS_FOR_PAGINATION, - ASC, - DESC, - SUB_QUERY_ALIAS_FOR_PAGINATION, -} from '../../../../constants'; -import { ResourceObject } from '../../../../types/response'; - -type OrderByCondition = Record; - -function getSortObject(params: any, relationName: string) { - return Object.entries(params).reduce((acum, [props, sort]) => { - acum[`${relationName}.${props}`] = `${sort}` === ASC ? ASC : DESC; - return acum; - }, {} as OrderByCondition); -} - -export async function getAll( - this: TypeormServiceObject, - query: Query -): Promise> { - const { fields, filter, include, sort, page } = query; - - let defaultSortObject: OrderByCondition = { - [`${ - this.typeormUtilsService.currentAlias - }.${this.typeormUtilsService.currentPrimaryColumn.toString()}`]: ASC, - }; - - const includeForCountQuery = new Set(); - const selectFields = new Set(); - const includeRel = new Set(); - - const skip = (page.number - 1) * page.size; - - const expressionArrayForTarget = - this.typeormUtilsService.getFilterExpressionForTarget(query); - const expressionArrayForRelation = - this.typeormUtilsService.getFilterExpressionForRelation(query); - const expressionArray = [ - ...expressionArrayForTarget, - ...expressionArrayForRelation, - ]; - - if (sort) { - const { target, ...relation } = sort; - const targetOrder = getSortObject( - target || {}, - this.typeormUtilsService.currentAlias - ); - - const relOrder = Object.entries(relation || {}).reduce( - (acum, [name, order]) => { - return { - ...acum, - ...getSortObject( - order || {}, - this.typeormUtilsService.getAliasForRelation(name) - ), - }; - }, - {} as OrderByCondition - ); - const resultOrder = { - ...targetOrder, - ...relOrder, - }; - if (Object.keys(resultOrder).length > 0) { - defaultSortObject = resultOrder; - } - for (const item of ObjectTyped.keys(relation)) { - includeForCountQuery.add(item.toString()); - } - } - - const queryBuilderForCount = this.repository - .createQueryBuilder(this.typeormUtilsService.currentAlias) - .select( - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - ), - this.typeormUtilsService.currentPrimaryColumn.toString() - ) - .orderBy(defaultSortObject); - - for (const i in expressionArray) { - const { params, alias, selectInclude, expression } = expressionArray[i]; - const expressionTempArray: string[] = []; - if (alias) { - expressionTempArray.push(alias); - } - expressionTempArray.push(expression); - queryBuilderForCount[i === '0' ? 'where' : 'andWhere']( - expressionTempArray.join(' ') - ); - if (params) { - if (Array.isArray(params)) { - for (const { name, val } of params) { - queryBuilderForCount.setParameters({ [name]: val }); - } - } else { - queryBuilderForCount.setParameters({ [params.name]: params.val }); - } - } - if (selectInclude) includeForCountQuery.add(selectInclude); - } - - for (const rel of [...includeForCountQuery]) { - const currentIncludeAlias = - this.typeormUtilsService.getAliasForRelation(rel); - queryBuilderForCount.leftJoin( - this.typeormUtilsService.getAliasPath(rel), - currentIncludeAlias - ); - } - - const count = await queryBuilderForCount.getCount(); - const meta = { - pageNumber: page.number, - totalItems: count, - pageSize: page.size, - }; - - if (count === 0) { - return { - meta, - data: [], - }; - } - - const aliasForIdResultPagination = this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn, - ALIAS_FOR_PAGINATION, - '_' - ); - - const resultIds = await this.repository - .createQueryBuilder(ALIAS_FOR_PAGINATION) - .select( - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn, - ALIAS_FOR_PAGINATION - ), - aliasForIdResultPagination - ) - .innerJoin( - `(${queryBuilderForCount.offset(skip).limit(page.size).getQuery()})`, - SUB_QUERY_ALIAS_FOR_PAGINATION, - `${this.typeormUtilsService.getAliasPath( - queryBuilderForCount.escape( - this.typeormUtilsService.currentPrimaryColumn.toString() - ), - queryBuilderForCount.escape(SUB_QUERY_ALIAS_FOR_PAGINATION) - )} = ${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn, - ALIAS_FOR_PAGINATION - )}` - ) - .setParameters(queryBuilderForCount.getParameters()) - .getRawMany<{ - [K: typeof aliasForIdResultPagination]: number; - }>(); - - const ids = resultIds.map((i) => i[aliasForIdResultPagination]); - if (ids.length === 0) { - return { - meta, - data: [], - }; - } - if (include) { - for (const rel of include) { - includeRel.add(rel); - } - } - - if (fields) { - if (include) { - for (const rel of include) { - const currentIncludeAlias = - this.typeormUtilsService.getAliasForRelation(rel); - const primaryColumnName = - this.typeormUtilsService.getPrimaryColumnForRel(rel); - selectFields.add(`${currentIncludeAlias}.${primaryColumnName}`); - } - } - - const { target, ...other } = fields; - if (target) { - for (const item of target) { - selectFields.add(`${this.typeormUtilsService.currentAlias}.${item}`); - } - } - - for (const [rel, fields] of ObjectTyped.entries(other)) { - const currentIncludeAlias = this.typeormUtilsService.getAliasForRelation( - rel as TupleOfEntityRelation[number] - ); - if (!fields) continue; - for (const field of fields) { - selectFields.add(`${currentIncludeAlias.toString()}.${field}`); - } - } - } - - const resultQuery = this.repository - .createQueryBuilder() - .orderBy(defaultSortObject); - - if (selectFields.size > 0) { - resultQuery.select([...selectFields]); - } - - resultQuery.whereInIds(ids); - for (const expressionItem of expressionArrayForRelation) { - const { selectInclude, alias, paramsForResult, params, expression } = - expressionItem; - if (paramsForResult) { - for (const item of paramsForResult) { - resultQuery.andWhere(item); - } - } else { - resultQuery.andWhere(`${alias} ${expression}`); - } - - if (params) { - if (Array.isArray(params)) { - for (const item of params) { - resultQuery.setParameters({ [item.name]: item.val }); - } - } else { - resultQuery.setParameters({ [params.name]: params.val }); - } - } - if (selectInclude) includeRel.add(selectInclude); - } - - for (const item of [...includeRel]) { - const currentIncludeAlias = - this.typeormUtilsService.getAliasForRelation(item); - if (!currentIncludeAlias) continue; - resultQuery[selectFields.size > 0 ? 'leftJoin' : 'leftJoinAndSelect']( - this.typeormUtilsService.getAliasPath(item), - currentIncludeAlias - ); - } - const resultData = await resultQuery.getMany(); - const { included, data } = - this.transformDataService.transformData(resultData); - return { - meta: { - pageNumber: page.number, - totalItems: count, - pageSize: page.size, - }, - data, - ...(included ? { included } : {}), - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts deleted file mode 100644 index 37dde9b2..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Equal, Repository } from 'typeorm'; - -import { Entity, TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - DEFAULT_PAGE_SIZE, - DEFAULT_QUERY_PAGE, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { Query, QueryField } from '../../../zod'; -import { NotFoundException } from '@nestjs/common'; -import { EntityPropsMapService } from '../../../../service'; - -function getDefaultQuery() { - const defaultQuery: Query = { - [QueryField.filter]: { - relation: null, - target: null, - }, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }; - - return defaultQuery; -} - -describe('getOne', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Get one item', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const query = getDefaultQuery(); - const checkData = await userRepository.findOne({ - where: { - id: Equal(1), - }, - relations: { - addresses: true, - comments: true, - }, - }); - query.include = ['addresses', 'comments']; - await typeormService.getOne('1', query); - expect(spyOnTransformData).toBeCalledWith(checkData); - }); - it('Get one item with select', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const query = getDefaultQuery(); - const checkData = await userRepository.findOne({ - select: { - firstName: true, - id: true, - isActive: true, - comments: { - id: true, - text: true, - }, - addresses: { - id: true, - }, - manager: { - id: true, - login: true, - }, - }, - where: { - id: Equal(1), - }, - relations: { - addresses: true, - comments: true, - manager: true, - }, - }); - query.include = ['addresses', 'comments', 'manager']; - query.fields = { - target: ['firstName', 'isActive'], - comments: ['text'], - manager: ['login'], - }; - await typeormService.getOne('1', query); - expect(spyOnTransformData).toBeCalledWith(checkData); - }); - it('Should be error', async () => { - expect.assertions(1); - try { - const query = getDefaultQuery(); - await typeormService.getOne('1000000', query); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts deleted file mode 100644 index 7e96c887..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { NotFoundException } from '@nestjs/common'; -import { - Entity, - ResourceObject, - TypeormServiceObject, - ValidateQueryError, -} from '../../../../types'; -import { Query } from '../../../zod'; -import { ObjectTyped } from '../../../utils'; - -export async function getOne( - this: TypeormServiceObject, - id: number | string, - query: Query -): Promise> { - const { include, fields } = query; - const selectFields = new Set(); - const builder = this.repository.createQueryBuilder( - this.typeormUtilsService.currentAlias - ); - - if (fields) { - selectFields.add( - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - ) - ); - - const { target, ...other } = fields; - if (target) { - for (const fieldItem of target) { - selectFields.add(this.typeormUtilsService.getAliasPath(fieldItem)); - } - } - - for (const [rel, fieldRel] of ObjectTyped.entries(other)) { - if (fieldRel) { - for (const itemFieldRel of fieldRel) { - selectFields.add( - this.typeormUtilsService.getAliasPath( - itemFieldRel, - this.typeormUtilsService.getAliasForRelation(rel.toString()) - ) - ); - } - } - } - } - - if (include) { - for (const rel of include) { - const currentIncludeAlias = - this.typeormUtilsService.getAliasForRelation(rel); - - builder[fields ? 'leftJoin' : 'leftJoinAndSelect']( - this.typeormUtilsService.getAliasPath(rel), - currentIncludeAlias - ); - - if (fields) { - selectFields.add( - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.getPrimaryColumnForRel(rel), - currentIncludeAlias - ) - ); - } - } - } - if (selectFields.size > 0) { - builder.select([...selectFields]); - } - const paramsId = 'paramsId'; - const result = await builder - .where( - `${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - )} = :${paramsId}`, - { - [paramsId]: id, - } - ) - .getOne(); - - if (!result) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, - path: ['fields'], - }; - throw new NotFoundException([error]); - } - const { included, data } = this.transformDataService.transformData(result); - return { - meta: {}, - data, - ...(included ? { included } : {}), - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts deleted file mode 100644 index 068cef6b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Repository } from 'typeorm'; - -import { TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; - -import { NotFoundException } from '@nestjs/common'; -import { EntityPropsMapService } from '../../../../service'; - -describe('getRelationship', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'getRelationships' - ); - const id = 1; - const rel = 'roles'; - const check = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - }, - where: { id }, - relations: { - roles: true, - }, - }); - const result = await typeormService.getRelationship(id, rel); - expect(spyOnTransformData).toBeCalledWith(check, rel); - expect(result).toHaveProperty('data'); - }); - it('Should be error', async () => { - expect.assertions(1); - try { - await typeormService.getRelationship('1000000', 'roles'); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts deleted file mode 100644 index 0478c488..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - Entity, - TypeormServiceObject, - EntityRelation, - ValidateQueryError, - ResourceObjectRelationships, -} from '../../../../types'; -import { - InternalServerErrorException, - NotFoundException, -} from '@nestjs/common'; - -export async function getRelationship< - E extends Entity, - Rel extends EntityRelation ->( - this: TypeormServiceObject, - id: number | string, - rel: Rel -): Promise> { - const paramsId = 'paramsId'; - const result = await this.repository - .createQueryBuilder() - .select([ - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - ), - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.getPrimaryColumnForRel(rel.toString()), - this.typeormUtilsService.getAliasForRelation(rel.toString()) - ), - ]) - .where( - ` - ${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - )} = :paramsId - ` - ) - .leftJoin( - this.typeormUtilsService.getAliasPath(rel.toString()), - this.typeormUtilsService.getAliasForRelation(rel.toString()) - ) - .setParameters({ - [paramsId]: id, - }) - .getOne(); - - if (!result) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, - path: ['fields'], - }; - throw new NotFoundException([error]); - } - - const { data } = this.transformDataService.getRelationships(result, rel); - if (data === undefined) { - const error: ValidateQueryError = { - code: 'custom', - message: `transformDataService.getRelationships return undefined`, - path: [], - }; - throw new InternalServerErrorException([error]); - } - return { - meta: {}, - data, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts deleted file mode 100644 index 866c347a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getAll } from './get-all/get-all'; -import { getOne } from './get-one/get-one'; -import { deleteOne } from './delete-one/delete-one'; -import { postOne } from './post-one/post-one'; -import { patchOne } from './patch-one/patch-one'; -import { getRelationship } from './get-relationship/get-relationship'; -import { postRelationship } from './post-relationship/post-relationship'; -import { deleteRelationship } from './delete-relationship/delete-relationship'; -import { patchRelationship } from './patch-relationship/patch-relationship'; -import { Entity, EntityRelation } from '../../../types'; - -export const MethodsService = { - getAll, - getOne, - deleteOne, - postOne, - patchOne, - getRelationship, - postRelationship, - deleteRelationship, - patchRelationship, -}; - -export type MethodsService = { - getAll: ( - ...arg: Parameters> - ) => ReturnType>; - getOne: ( - ...arg: Parameters> - ) => ReturnType>; - deleteOne: ( - ...arg: Parameters> - ) => ReturnType>; - postOne: ( - ...arg: Parameters> - ) => ReturnType>; - patchOne: ( - ...arg: Parameters> - ) => ReturnType>; - getRelationship: >( - ...arg: Parameters> - ) => ReturnType>; - postRelationship: >( - ...arg: Parameters> - ) => ReturnType>; - deleteRelationship: >( - ...arg: Parameters> - ) => ReturnType>; - patchRelationship: >( - ...arg: Parameters> - ) => ReturnType>; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts deleted file mode 100644 index 38cb8cf3..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IBackup, IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; - -import { TypeormService } from '../../../../types'; -import { PatchData, PostData } from '../../../../helper/zod'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { EntityPropsMapService } from '../../../../service'; - -describe('patchOne', () => { - let db: IMemoryDb; - let backaUp: IBackup; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - const firstName = 'firstName test'; - const isActive = false; - const testDate = new Date(); - const login = 'login test'; - - let inputData: PostData; - let newData: PatchData; - - let notes: Notes[]; - let users: Users[]; - let roles: Roles[]; - let userGroup: UserGroups[]; - let addresses: Addresses[]; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - - notes = await notesRepository.find(); - users = await userRepository.find(); - roles = await rolesRepository.find(); - userGroup = await userGroupRepository.find({ - relations: { - users: true, - }, - }); - addresses = await addressesRepository.find(); - - inputData = { - type: 'users', - attributes: { - firstName, - isActive, - testDate, - login, - }, - relationships: { - addresses: { - type: 'addresses', - id: addresses[0].id.toString(), - }, - notes: [ - { - type: 'notes', - id: notes[0].id, - }, - ], - roles: [ - { - type: 'roles', - id: `${roles[0].id}`, - }, - ], - manager: { - type: 'users', - id: `${users[0].id}`, - }, - userGroup: { - type: 'user-group', - id: `${userGroup[0].id}`, - }, - }, - }; - - await typeormService.postOne(inputData); - backaUp = db.backup(); - const changeUser = await userRepository.findOneBy({ - login: inputData.attributes.login as string, - }); - if (!changeUser) { - throw new Error('not found mock data'); - } - newData = { - ...inputData, - id: `${changeUser.id}`, - }; - const newLogin = `${changeUser.login} - newLogin`; - const newIsActive = !changeUser.isActive; - - newData.attributes.login = newLogin; - newData.attributes.isActive = newIsActive; - newData.attributes.testDate = new Date(); - - newData.relationships = { - ...newData.relationships, - manager: { - type: 'users', - id: users[1].id.toString(), - }, - addresses: null, - userGroup: { - type: 'user-group', - id: `${userGroup[1].id}`, - }, - roles: [ - { - type: 'roles', - id: `${roles[1].id}`, - }, - ], - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - backaUp.restore(); - }); - - it('should be ok without relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - })); - - const { relationships, ...withoutRelationships } = newData; - const returnData = await typeormService.patchOne( - withoutRelationships.id, - withoutRelationships - ); - - const result = await userRepository.findOneBy({ - id: parseInt(withoutRelationships.id, 10), - }); - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).not.toHaveProperty('included'); - }); - - it('should be ok with relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - included: {} as any, - })); - - const returnData = await typeormService.patchOne(newData.id, newData); - - const result = await userRepository.findOne({ - where: { - id: parseInt(newData.id, 10), - }, - relations: { - addresses: true, - notes: true, - userGroup: true, - roles: true, - manager: true, - }, - }); - - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).toHaveProperty('included'); - }); - - it('should be ok with relation nulling relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - included: {} as any, - })); - - newData.relationships = { - ...newData.relationships, - userGroup: null, - roles: [], - }; - - const returnData = await typeormService.patchOne(newData.id, newData); - - const result = await userRepository.findOne({ - where: { - id: parseInt(newData.id, 10), - }, - relations: { - addresses: true, - notes: true, - userGroup: true, - roles: true, - manager: true, - }, - }); - - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).toHaveProperty('included'); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts deleted file mode 100644 index 6045e02f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; -import { DeepPartial } from 'typeorm'; -import { - Entity, - ResourceObject, - TypeormServiceObject, - ValidateQueryError, -} from '../../../../types'; -import { PatchData } from '../../../zod'; -import { ObjectTyped } from '../../../utils'; - -export async function patchOne( - this: TypeormServiceObject, - id: number | string, - inputData: PatchData -): Promise> { - const { id: idBody, attributes, relationships } = inputData; - - if (`${id}` !== idBody) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Data 'id' must be equal to url param`, - path: ['data', 'id'], - }; - - throw new UnprocessableEntityException([error]); - } - - const paramsId = 'paramsId'; - const result = await this.repository - .createQueryBuilder() - .where( - `${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - )} = :${paramsId}`, - { - [paramsId]: id, - } - ) - .getOne(); - - if (!result) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, - path: ['data', 'id'], - }; - throw new NotFoundException([error]); - } - - if (attributes) { - const entityTarget = this.repository.manager.create( - this.repository.target, - attributes as DeepPartial - ); - for (const [props, val] of ObjectTyped.entries(entityTarget)) { - result[props] = val; - } - } - - const saveData = await this.typeormUtilsService.saveEntityData( - result, - relationships - ); - - const { data, included } = this.transformDataService.transformData(saveData); - const includeData = included ? { included } : {}; - return { - meta: {}, - data, - ...includeData, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts deleted file mode 100644 index a7c55db0..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Repository } from 'typeorm'; - -import { TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { EntityPropsMapService } from '../../../../service'; - -describe('patchRelationship', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - let typeormUtilsService: TypeormUtilsService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - typeormUtilsService = - module.get>(TypeormUtilsService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', async () => { - const checkUser = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - - const roles = await rolesRepository.find(); - const userGroups = await userGroupRepository.find(); - const users = await userRepository.find(); - - if (!checkUser) { - throw new Error('not found mock'); - } - - const userGroupData = { - type: 'user-groups', - id: userGroups - .find((i) => checkUser.userGroup.id !== i.id) - ?.id.toString(), - }; - const rolesData = [ - { - type: 'roles', - id: roles - .find((i) => checkUser.roles.find((a) => a.id !== i.id)) - ?.id.toString(), - }, - ]; - - const managerData = { - type: 'users', - id: users.find((i) => checkUser.manager.id !== i.id)?.id.toString(), - }; - const result = await typeormService.patchRelationship( - 1, - 'roles', - rolesData as any - ); - const result1 = await typeormService.patchRelationship( - 1, - 'userGroup', - userGroupData as any - ); - const result2 = await typeormService.patchRelationship( - 1, - 'manager', - managerData as any - ); - - const checkUserAfterPost = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - if (!checkUserAfterPost) { - throw new Error('not found'); - } - expect(checkUserAfterPost.manager.id.toString()).toBe(managerData.id); - expect(checkUserAfterPost.roles.map((i) => i.id.toString())).toEqual( - rolesData.map((i) => i.id) - ); - expect(checkUserAfterPost.userGroup.id.toString()).toBe(userGroupData.id); - - await typeormService.patchRelationship(1, 'roles', []); - await typeormService.patchRelationship(1, 'manager', null); - const checkUserAfterPatch = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - if (!checkUserAfterPatch) { - throw new Error('not found'); - } - - expect(checkUserAfterPatch.manager).toBe(null); - expect(checkUserAfterPatch.roles).toEqual([]); - expect(result.data.map((i) => i.id)).toEqual( - checkUserAfterPost.roles.map((i) => i.id.toString()) - ); - expect(result2.data?.id).toEqual(checkUserAfterPost.manager.id.toString()); - expect(result1.data?.id).toEqual( - checkUserAfterPost.userGroup.id.toString() - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts deleted file mode 100644 index 13f77e8a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - Entity, - TypeormServiceObject, - EntityRelation, - ResourceObjectRelationships, -} from '../../../../types'; - -import { PatchRelationshipData } from '../../../zod'; -import { getRelationship } from '../get-relationship/get-relationship'; - -export async function patchRelationship< - E extends Entity, - Rel extends EntityRelation ->( - this: TypeormServiceObject, - id: number | string, - rel: Rel, - input: PatchRelationshipData -): Promise> { - const idsResult = await this.typeormUtilsService.validateRelationInputData( - rel, - input - ); - - const patchBuilder = this.repository - .createQueryBuilder() - .relation(rel.toString()) - .of(id); - - if (Array.isArray(idsResult)) { - const data = await getRelationship.call< - TypeormServiceObject, - [number | string, Rel], - Promise> - >(this, id, rel); - const idsToDelete = Array.isArray(data.data) - ? data.data.map((i) => i.id) - : []; - - await patchBuilder.addAndRemove(idsResult, idsToDelete); - } else { - await patchBuilder.set(idsResult); - } - - return getRelationship.call< - TypeormServiceObject, - [number | string, Rel], - Promise> - >(this, id, rel); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts deleted file mode 100644 index 084e375f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IBackup, IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - Pods, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; - -import { TypeormService } from '../../../../types'; -import { PostData } from '../../../../helper/zod'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { EntityPropsMapService } from '../../../../service'; - -describe('postOne', () => { - let db: IMemoryDb; - let backaUp: IBackup; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - let podsRepository: Repository; - - let typeormServicePods: TypeormService; - let transformDataServicePods: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - const firstName = 'firstName test'; - const isActive = false; - const testDate = new Date(); - const login = 'login test'; - - let inputData: PostData; - - let notes: Notes[]; - let users: Users[]; - let roles: Roles[]; - let userGroup: UserGroups[]; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - - const modulePods: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Pods), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Pods), - EntityPropsMapService, - ], - }).compile(); - - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - podsRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - backaUp = db.backup(); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - - typeormServicePods = modulePods.get>(TYPEORM_SERVICE); - transformDataServicePods = - modulePods.get>(TransformDataService); - - notes = await notesRepository.find(); - users = await userRepository.find(); - roles = await rolesRepository.find(); - userGroup = await userGroupRepository.find(); - - inputData = { - type: 'users', - attributes: { - firstName, - isActive, - testDate, - login, - }, - relationships: { - notes: [ - { - type: 'notes', - id: notes[0].id, - }, - ], - roles: [ - { - type: 'roles', - id: `${roles[0].id}`, - }, - ], - manager: { - type: 'users', - id: `${users[0].id}`, - }, - userGroup: { - type: 'user-group', - id: `${userGroup[0].id}`, - }, - }, - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - backaUp.restore(); - }); - - it('should be ok without relation and with id', async () => { - const spyOnTransformData = jest - .spyOn(transformDataServicePods, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - })); - const { relationships, ...other } = inputData; - const id = '5'; - const returnData = await typeormServicePods.postOne({ - id, - type: 'pods', - attributes: { - name: 'test', - }, - }); - const result = await podsRepository.findOneBy({ - id, - }); - - expect(spyOnTransformData).toBeCalledWith({ - ...result, - id, - }); - expect(returnData).not.toHaveProperty('included'); - }); - - it('should be ok without relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - })); - const { relationships, ...other } = inputData; - const returnData = await typeormService.postOne(other); - const result = await userRepository.findOneBy({ - login, - }); - - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).not.toHaveProperty('included'); - }); - - it('should be ok with relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - included: {} as any, - })); - const returnData = await typeormService.postOne(inputData); - const result = await userRepository.findOne({ - where: { - login, - }, - relations: { - notes: true, - userGroup: true, - roles: true, - manager: true, - }, - }); - - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).toHaveProperty('included'); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts deleted file mode 100644 index 97ba0a7c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { DeepPartial } from 'typeorm'; -import { - Entity, - ResourceObject, - TypeormServiceObject, -} from '../../../../types'; -import { PostData } from '../../../zod'; - -export async function postOne( - this: TypeormServiceObject, - inputData: PostData -): Promise> { - const { attributes, relationships, id } = inputData; - - const idObject = id - ? { [this.typeormUtilsService.currentPrimaryColumn.toString()]: id } - : {}; - - const attributesObject = { - ...attributes, - ...idObject, - } as DeepPartial; - - const entityTarget = this.repository.manager.create( - this.repository.target, - attributesObject - ); - - const saveData = await this.typeormUtilsService.saveEntityData( - entityTarget, - relationships - ); - - const { data, included } = this.transformDataService.transformData(saveData); - const includeData = included ? { included } : {}; - return { - meta: {}, - data, - ...includeData, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts deleted file mode 100644 index 966c7b0b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Repository } from 'typeorm'; - -import { TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { EntityPropsMapService } from '../../../../service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; - -describe('postRelationship', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - let typeormUtilsService: TypeormUtilsService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - typeormUtilsService = - module.get>(TypeormUtilsService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', async () => { - const checkUser = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - - const roles = await rolesRepository.find(); - const userGroups = await userGroupRepository.find(); - const users = await userRepository.find(); - - if (!checkUser) { - throw new Error('not found mock'); - } - - const userGroupData = { - type: 'user-groups', - id: userGroups - .find((i) => checkUser.userGroup.id !== i.id) - ?.id.toString(), - }; - const rolesData = [ - { - type: 'roles', - id: roles - .find((i) => checkUser.roles.find((a) => a.id !== i.id)) - ?.id.toString(), - }, - ]; - - const managerData = { - type: 'users', - id: users.find((i) => checkUser.manager.id !== i.id)?.id.toString(), - }; - const result = await typeormService.postRelationship( - 1, - 'roles', - rolesData as any - ); - const result1 = await typeormService.postRelationship( - 1, - 'userGroup', - userGroupData as any - ); - - const result2 = await typeormService.postRelationship( - 1, - 'manager', - managerData as any - ); - - const checkUserAfterPost = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - if (!checkUserAfterPost) { - throw new Error('not found'); - } - - expect(checkUserAfterPost.manager.id.toString()).toBe(managerData.id); - expect(checkUserAfterPost.roles.map((i) => i.id.toString())).toEqual([ - ...checkUser.roles.map((i) => i.id.toString()), - ...rolesData.map((i) => i.id), - ]); - expect(checkUserAfterPost.userGroup.id.toString()).toBe(userGroupData.id); - - expect(result.data.map((i) => i.id)).toEqual( - checkUserAfterPost.roles.map((i) => i.id.toString()) - ); - expect(result2.data?.id).toEqual(checkUserAfterPost.manager.id.toString()); - expect(result1.data?.id).toEqual( - checkUserAfterPost.userGroup.id.toString() - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts deleted file mode 100644 index 11652e12..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - Entity, - TypeormServiceObject, - EntityRelation, - ResourceObjectRelationships, -} from '../../../../types'; - -import { PostRelationshipData } from '../../../zod'; -import { getRelationship } from '../get-relationship/get-relationship'; - -export async function postRelationship< - E extends Entity, - Rel extends EntityRelation ->( - this: TypeormServiceObject, - id: number | string, - rel: Rel, - input: PostRelationshipData -): Promise> { - const idsResult = await this.typeormUtilsService.validateRelationInputData( - rel, - input - ); - const postBuilder = this.repository - .createQueryBuilder() - .relation(rel.toString()) - .of(id); - - if (Array.isArray(idsResult)) { - await postBuilder.add(idsResult); - } else { - await postBuilder.set(idsResult); - } - - return getRelationship.call< - TypeormServiceObject, - [number | string, Rel], - Promise> - >(this, id, rel); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts deleted file mode 100644 index 525a4cab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { - mockDBTestModule, - createAndPullSchemaBase, - pullUser, - pullAllData, - providerEntities, - getRepository, - Users, - Addresses, - Notes, - Comments, - Roles, - UserGroups, -} from '../../mock-utils'; -import { EntityProps, EntityRelation, TypeOfArray } from '../../types'; -import { - getField, - getPropsTreeForRepository, - fromRelationTreeToArrayName, - getArrayFields, - PropsArray, - getArrayPropsForEntity, - ArrayPropsForEntity, - getFieldWithType, - getRelationTypeArray, - getRelationTypeName, - getRelationTypePrimaryColumn, - TypeField, - getTypePrimaryColumn, - getPrimaryColumnsForRelation, - getIsArrayRelation, - getTypeForAllProps, - getPropsFromDb, -} from './orm-helper'; -import { ObjectTyped } from '../utils'; - -describe('zod-helper', () => { - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - let db: IMemoryDb; - let user: Users; - let userWithRelation: Users; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [...providerEntities(getDataSourceToken())], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - - user = await pullUser(userRepository); - userWithRelation = await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - }); - - it('getField', async () => { - const { relations, field } = getField(userRepository); - const userFieldProps = Object.getOwnPropertyNames( - user - ) as EntityProps[]; - const hasUserFieldInResultField = userFieldProps.some( - (field) => !field.includes(field) - ); - - const hasResultInUserField = field.some( - (field) => !userFieldProps.includes(field) - ); - - const userRelationProps: EntityRelation[] = ( - Object.getOwnPropertyNames(userWithRelation) as (EntityProps & - EntityRelation)[] - ).filter((props) => !userFieldProps.includes(props)); - - const hasUserRelationInResultField = userRelationProps.some( - (field) => !relations.includes(field) - ); - - const hasResultInUserRelation = relations.some( - (field) => !userRelationProps.includes(field) - ); - - expect(hasUserFieldInResultField).toEqual(false); - expect(hasResultInUserField).toEqual(false); - - expect(hasUserRelationInResultField).toEqual(false); - expect(hasResultInUserRelation).toEqual(false); - }); - - it('getPropsTreeForRepository', () => { - const relationField = getPropsTreeForRepository(userRepository); - const userFieldProps = Object.getOwnPropertyNames( - user - ) as EntityProps[]; - const userRelationProps: EntityRelation[] = ( - Object.getOwnPropertyNames(userWithRelation) as (EntityProps & - EntityRelation)[] - ).filter((props) => !userFieldProps.includes(props)); - - const hasUserRelationInResultField = userRelationProps.some( - (field) => !Object.keys(relationField).includes(field) - ); - const hasResultInUserRelation = ObjectTyped.keys(relationField).some( - (field) => !userRelationProps.includes(field) - ); - expect(hasUserRelationInResultField).toEqual(false); - expect(hasResultInUserRelation).toEqual(false); - - for (const [relationName, fieldsRelation] of ObjectTyped.entries( - relationField - )) { - const check = fieldsRelation.some((field) => { - const targetItem = userWithRelation[relationName]; - const target = Array.isArray(targetItem) ? targetItem[0] : targetItem; - // @ts-ignore - return !ObjectTyped.keys(target).includes(field); - }); - expect(check).toEqual(false); - } - }); - - it('fromRelationTreeToArrayName', () => { - const { relations, field } = getField(userRepository); - - const relationField = getPropsTreeForRepository(userRepository); - const checkArray = fromRelationTreeToArrayName(relationField); - - for (const key of relations) { - let resultKey = - key === 'manager' ? 'Users' : key === 'userGroup' ? 'UserGroups' : key; - - const relationsRepo = - userRepository.metadata.connection.getRepository< - TypeOfArray - >(resultKey); - const { field: relationsFields } = getField(relationsRepo); - const textField = relationsFields.map((r) => `${key}.${r}`); - const check = textField.some((i) => !checkArray.includes(i as any)); - expect(check).toEqual(false); - } - }); - - it('getArrayFields', () => { - const result = getArrayFields(addressesRepository); - expect(result).toEqual({ - arrayField: true, - } as PropsArray); - }); - - it('getArrayPropsForEntity', () => { - const result = getArrayPropsForEntity(userRepository); - const check: ArrayPropsForEntity = { - target: { - testReal: true, - testArrayNull: true, - }, - manager: { - testReal: true, - testArrayNull: true, - }, - comments: {}, - notes: {}, - userGroup: {}, - roles: {}, - addresses: { - arrayField: true, - }, - }; - expect(result).toEqual(check); - }); - - it('getFieldWithType', () => { - const result = getFieldWithType(addressesRepository); - expect(result.arrayField).toBe('array'); - expect(result.state).toBe('string'); - expect(result.id).toBe('number'); - expect(result.createdAt).toBe('date'); - const result2 = getFieldWithType(userRepository); - - expect(result2.isActive).toBe('boolean'); - }); - - it('getRelationType', () => { - const result = getRelationTypeArray(userRepository); - expect(result.roles).toBe(true); - expect(result.comments).toBe(true); - expect(result.manager).toBe(false); - expect(result.addresses).toBe(false); - expect(result.userGroup).toBe(false); - expect(result.notes).toBe(true); - }); - - it('getRelationTypeName', () => { - const result = getRelationTypeName(userRepository); - expect(result.roles).toBe('Roles'); - expect(result.comments).toBe('Comments'); - expect(result.manager).toBe('Users'); - expect(result.addresses).toBe('Addresses'); - expect(result.userGroup).toBe('UserGroups'); - expect(result.notes).toBe('Notes'); - }); - - it('getRelationTypePrimaryColumn', () => { - const result = getRelationTypePrimaryColumn(userRepository); - expect(result.roles).toBe(TypeField.number); - expect(result.comments).toBe(TypeField.number); - expect(result.manager).toBe(TypeField.number); - expect(result.addresses).toBe(TypeField.number); - expect(result.userGroup).toBe(TypeField.number); - expect(result.notes).toBe(TypeField.string); - }); - - it('getTypePrimaryColumn', () => { - expect(getTypePrimaryColumn(userRepository)).toBe(TypeField.number); - expect(getTypePrimaryColumn(notesRepository)).toBe(TypeField.string); - }); - - it('getPrimaryColumnsForRelation', () => { - const result = getPrimaryColumnsForRelation(userRepository); - expect(result.roles).toBe('id'); - expect(result.manager).toBe('id'); - }); - - it('geTisArrayRelation', () => { - const result = getIsArrayRelation(userRepository); - expect(result).toEqual({ - addresses: false, - manager: false, - roles: true, - comments: true, - notes: true, - userGroup: false, - }); - }); - - it('getTypeForAllProps', () => { - const result = getTypeForAllProps(userRepository); - expect(result.manager.id).toBe(TypeField.number); - expect(result.testDate).toBe(TypeField.date); - expect(result.comments.id).toBe(TypeField.number); - expect(result.notes.id).toBe(TypeField.string); - }); - - it('getPropsFromDb', () => { - const result = getPropsFromDb(userRepository); - expect(result['testReal']).toEqual({ - type: 'real', - isArray: true, - isNullable: false, - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts deleted file mode 100644 index d73f0835..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { Repository } from 'typeorm'; -import { Type } from '@nestjs/common'; -import { - CastProps, - Concat, - Entity, - EntityProps, - EntityPropsArray, - EntityRelation, - IsArray, - TypeCast, - TypeOfArray, - UnionToTuple, - ValueOf, -} from '../../types'; -import { getEntityName, ObjectTyped } from '../utils'; -import { guardKeyForPropertyTarget } from './orm-type-asserts'; -import { ColumnType } from 'typeorm/driver/types/ColumnTypes'; - -export enum PropsNameResultField { - field = 'field', - relations = 'relations', -} - -export type ResultGetField = { - [PropsNameResultField.field]: TupleOfEntityProps; - [PropsNameResultField.relations]: TupleOfEntityRelation; -}; - -export type TupleOfEntityProps< - E, - Props = UnionToTuple> -> = Props extends readonly [string, ...string[]] ? Props : never; -export type TupleOfEntityRelation< - E, - Props = UnionToTuple> -> = Props extends readonly [string, ...string[]] ? Props : never; - -export type RelationTree = { - [K in keyof RelationType]: TypeOfArray extends Entity - ? ResultGetField>['field'] - : never; -}; - -export type ConcatFieldWithRelation< - R extends string, - T extends readonly string[] -> = ValueOf<{ - [K in T[number]]: Concat; -}>; - -export type ConcatRelationUnion< - E extends Entity, - R = RelationTree -> = ValueOf<{ - [K in keyof R]: ConcatFieldWithRelation< - TypeCast, - TypeCast - >; -}>; - -export type ConcatRelation = TypeCast< - UnionToTuple>, - [string, ...string[]] ->; - -type RelationType = { - [K in EntityRelation]: Type>>; -}; - -export enum TypeField { - array = 'array', - date = 'date', - number = 'number', - boolean = 'boolean', - string = 'string', - object = 'object', -} - -export type TypeForId = Extract; - -export type FieldWithType = { - [K in EntityProps]: IsArray extends true - ? TypeField.array - : E[K] extends Date - ? TypeField.date - : E[K] extends number - ? TypeField.number - : E[K] extends boolean - ? TypeField.boolean - : E[K] extends object - ? TypeField.object - : TypeField.string; -}; - -export type RelationPropsType = { - [K in EntityRelation]: E[K] extends unknown[] ? true : false; -}; - -export type RelationPropsTypeName = { - [K in EntityRelation]: string; -}; - -export type RelationPrimaryColumnType = { - [K in EntityRelation]: TypeForId; -}; - -export const getRelationTypeArray = ( - repository: Repository -): RelationPropsType => { - const { relations } = getField(repository); - - const entity = repository.target as any; - const result = {} as any; - for (const item of relations) { - result[item] = - Reflect.getMetadata('design:type', entity['prototype'], item) === Array; - } - return result; -}; - -export const getTypePrimaryColumn = ( - repository: Repository -): TypeForId => { - const target = repository.target as any; - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - return Reflect.getMetadata( - 'design:type', - target['prototype'], - primaryColumn - ) === Number - ? TypeField.number - : TypeField.string; -}; - -export const getRelationTypePrimaryColumn = ( - repository: Repository -): RelationPrimaryColumnType => { - return repository.metadata.relations.reduce((acum, i) => { - const target = i.inverseEntityMetadata.target as any; - const primaryColumn = - i.inverseEntityMetadata.primaryColumns[0].propertyName; - acum[i.propertyName] = - Reflect.getMetadata('design:type', target['prototype'], primaryColumn) === - Number - ? TypeField.number - : TypeField.string; - return acum; - }, {} as Record) as RelationPrimaryColumnType; -}; - -export const getPrimaryColumnsForRelation = ( - repository: Repository -): RelationPropsTypeName => { - return repository.metadata.relations.reduce((acum, i) => { - const target = i.inverseEntityMetadata.target as any; - acum[i.propertyName] = - i.inverseEntityMetadata.primaryColumns[0].propertyName; - return acum; - }, {} as Record) as RelationPropsTypeName; -}; - -export const getRelationTypeName = ( - repository: Repository -): RelationPropsTypeName => { - return repository.metadata.relations.reduce((acum, i) => { - acum[i.propertyName] = getEntityName(i.inverseEntityMetadata.target); - return acum; - }, {} as Record) as RelationPropsTypeName; -}; - -export const getFieldWithType = ( - repository: Repository -): FieldWithType => { - const { field } = getField(repository); - - const entity = repository.target as any; - const result = {} as any; - for (const item of field) { - let typeProps: TypeField = TypeField.string; - switch (Reflect.getMetadata('design:type', entity['prototype'], item)) { - case Array: - typeProps = TypeField.array; - break; - case Date: - typeProps = TypeField.date; - break; - case Number: - typeProps = TypeField.number; - break; - case Boolean: - typeProps = TypeField.boolean; - break; - case Object: - typeProps = TypeField.object; - break; - default: - typeProps = TypeField.string; - } - result[item] = typeProps; - } - - return result; -}; - -export const getField = ( - repository: Repository -): ResultGetField => { - const relations = repository.metadata.relations.map((i) => { - return i.propertyName; - }); - - const field = repository.metadata.columns - .filter((i) => !relations.includes(i.propertyName)) - .map((r) => r.propertyName); - - return { - field, - relations, - } as unknown as ResultGetField; -}; - -export type PropsArray = { [K in EntityPropsArray]: true }; - -export const getArrayFields = ( - repository: Repository -): PropsArray => { - const relations = repository.metadata.relations.map((i) => { - return i.propertyName; - }); - - return repository.metadata.columns - .filter((i) => !relations.includes(i.propertyName)) - .reduce((acum, metaData) => { - if (metaData.isArray) { - acum[metaData.propertyName] = true; - } - return acum; - }, {} as Record) as PropsArray; -}; - -export type ArrayPropsForEntity = { - target: PropsArray; -} & { - [K in ResultGetField['relations'][number]]: PropsArray< - TypeOfArray> - >; -}; - -export type PropertyTarget< - E extends Entity, - For extends PropsNameResultField -> = { - [K in ResultGetField[For][number]]: K extends keyof E - ? TypeOfArray - : never; -}; - -export const getArrayPropsForEntity = ( - repository: Repository -): ArrayPropsForEntity => { - const connection = repository.metadata.connection; - - const relationsTargets = repository.metadata.relations.reduce( - (acum, i) => ({ - ...acum, - [i.propertyName]: i.type, - }), - {} as Record - ) as PropertyTarget; - - const { relations } = getField(repository); - const relationsArrayFields = relations.reduce( - (acum, item) => { - guardKeyForPropertyTarget(relationsTargets, item); - const target = relationsTargets[item] as TypeCast< - TypeOfArray>, - Entity - >; - - const repository = connection.getRepository( - target as Function - ) as Repository; - - acum[item] = getArrayFields(repository) as PropsArray< - TypeOfArray> - >; - - return acum; - }, - {} as { - [K in ResultGetField['relations'][number]]: PropsArray< - TypeOfArray> - >; - } - ); - - return { - target: getArrayFields(repository), - ...relationsArrayFields, - }; -}; - -export const getPropsTreeForRepository = ( - repository: Repository -): RelationTree => { - const dataSource = repository.metadata.connection; - - const relationType = repository.metadata.relations.reduce((acum, i) => { - acum[i.propertyName] = i.inverseEntityMetadata.target; - return acum; - }, {} as Record) as unknown as RelationType; - - return ObjectTyped.entries(relationType).reduce( - (acum, [key, value]) => ({ - ...acum, - ...{ [key]: getField(dataSource.getRepository(value))['field'] }, - }), - {} as RelationTree - ); -}; - -export const getIsArrayRelation = ( - repository: Repository -): { - [K in EntityRelation]: E[K] extends unknown[] ? true : false; -} => { - return repository.metadata.relations.reduce((acum, i) => { - switch (i.relationType) { - case 'one-to-many': - case 'many-to-many': - acum[i.propertyName] = true; - break; - default: - acum[i.propertyName] = false; - } - return acum; - }, {} as any) as any; -}; - -export const fromRelationTreeToArrayName = ( - relationTree: RelationTree -): ConcatRelation => { - return ObjectTyped.entries(relationTree).reduce((acum, [name, filed]) => { - acum.push(...filed.map((i) => `${name.toLocaleString()}.${i}`)); - return acum; - }, [] as string[]) as unknown as ConcatRelation; -}; - -export type AllFieldWithTpe = FieldWithType & { - [K in EntityRelation]: E[K] extends (infer U extends Entity)[] - ? FieldWithType - : E[K] extends Entity - ? FieldWithType - : never; -}; - -export const getTypeForAllProps = ( - repository: Repository -): AllFieldWithTpe => { - const targetField = getFieldWithType(repository); - - const relationField = repository.metadata.relations.reduce((acum, item) => { - acum[item.propertyName] = getFieldWithType( - repository.manager.getRepository(item.inverseEntityMetadata.target) - ); - return acum; - }, {} as any); - - return { - ...targetField, - ...relationField, - }; -}; - -export type PropsFieldItem = { - type: ColumnType; - isArray: boolean; - isNullable: boolean; -}; - -export type PropsForField = { - [K in EntityProps]: PropsFieldItem; -}; - -export const getPropsFromDb = ( - repository: Repository -): PropsForField => { - return repository.metadata.columns.reduce((acum, i) => { - acum[i.propertyName as EntityProps] = { - type: i.type, - isArray: i.isArray, - isNullable: i.isNullable, - }; - return acum; - }, {} as PropsForField); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts deleted file mode 100644 index 779f339d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Entity } from '../../types'; -import { - PropsNameResultField, - PropertyTarget, -} from './orm-helper'; - -export function guardKeyForPropertyTarget< - E extends Entity, - For extends PropsNameResultField, - R extends PropertyTarget ->(relationsTargets: R, key: any): asserts key is keyof R { - if (!(key in relationsTargets)) throw new Error('Type guard error'); -} - -export function guardIsKeyOfObject( - object: R, - key: string | number | symbol -): asserts key is keyof R { - if (typeof object === 'object' && object !== null && key in object) - return void 0; - - throw new Error('Type guard error'); -} - -export function guardIsArray(input: T|Array): input is Array{ - return Array.isArray(input) -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts deleted file mode 100644 index bba7eaba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { FilterOperand as FilterOperandType } from '../../types'; - -const title = 'is equal to the conditional of query'; - -export const OperandsMapTitle = { - [FilterOperandType.in]: `${title} "WHERE 'attribute_name' IN ('value1', 'value2')"`, - [FilterOperandType.nin]: `${title} "WHERE 'attribute_name' NOT IN ('value1', 'value1')"`, - [FilterOperandType.eq]: `${title} "WHERE 'attribute_name' = 'value1'`, - [FilterOperandType.ne]: `${title} "WHERE 'attribute_name' <> 'value1'`, - [FilterOperandType.gt]: `${title} "WHERE 'attribute_name' > 'value1'`, - [FilterOperandType.gte]: `${title} "WHERE 'attribute_name' >= 'value1'`, - [FilterOperandType.like]: `${title} "WHERE 'attribute_name' ILIKE %value1%`, - [FilterOperandType.lt]: `${title} "WHERE 'attribute_name' < 'value1'`, - [FilterOperandType.lte]: `${title} "WHERE 'attribute_name' <= 'value1'`, - [FilterOperandType.regexp]: `${title} "WHERE 'attribute_name' ~* value1`, - [FilterOperandType.some]: `${title} "WHERE 'attribute_name' && [value1]`, -}; - -export class FilterOperand { - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.in], - required: false, - type: 'array', - items: { - type: 'string', - }, - }) - [FilterOperandType.in]!: string[]; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.nin], - required: false, - type: 'array', - items: { - type: 'string', - }, - }) - [FilterOperandType.nin]!: string[]; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.eq], - required: false, - }) - [FilterOperandType.eq]!: string; - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.ne], - required: false, - }) - [FilterOperandType.ne]!: string; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.gte], - required: false, - }) - [FilterOperandType.gte]!: string; - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.gt], - required: false, - }) - [FilterOperandType.gt]!: string; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.lt], - required: false, - }) - [FilterOperandType.lt]!: string; - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.lte], - required: false, - }) - [FilterOperandType.lte]!: string; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.regexp], - required: false, - }) - [FilterOperandType.regexp]!: string; - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.some], - required: false, - }) - [FilterOperandType.some]!: string; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts deleted file mode 100644 index e0bff43c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -// import { Type } from '@nestjs/common'; -// import { DECORATORS } from '@nestjs/swagger/dist/constants'; -// import { ApiTags, ApiExtraModels } from '@nestjs/swagger'; -// // import { camelToKebab, ObjectTyped } from '../utils'; -// // import { FilterOperand } from './filter-operand-model'; -// // import { JSON_API_DECORATOR_OPTIONS } from '../../constants'; -// // import { DecoratorOptions, Entity, MethodName, ConfigParam } from '../../types'; -// import { Bindings } from '../../config/bindings'; -// import { swaggerMethod } from './method'; -// // import { createApiModels } from './utils'; -// -// import { Entity, ConfigParam } from '../../types'; -// import { ObjectTyped } from '../utils'; -// -// export function setSwaggerDecorator( -// controller: Type, -// entity: Entity, -// config: ConfigParam -// ) { -// // const apiTag = Reflect.getMetadata(DECORATORS.API_TAGS, controller); -// // if (!apiTag) { -// // const entityName = -// // entity instanceof Function ? entity.name : entity.options.name; -// // -// // ApiTags(config?.['overrideRoute'] || `${camelToKebab(entityName)}`)( -// // controller -// // ); -// // } -// // ApiExtraModels(FilterOperand)(controller); -// // ApiExtraModels(createApiModels(entity))(controller); -// // -// // const decoratorOptions: DecoratorOptions = Reflect.getMetadata( -// // JSON_API_DECORATOR_OPTIONS, -// // controller -// // ); -// // -// for (const method of ObjectTyped.keys(Bindings)) { -// // if (decoratorOptions) { -// // const { allowMethod = Object.keys(Bindings) } = decoratorOptions; -// // -// // if (!allowMethod.includes(method as MethodName)) { -// // continue; -// // } -// // } -// // -// if (method in swaggerMethod) { -// swaggerMethod[method](controller, entity, Bindings[method], config); -// } -// } -// } - -export * from './method'; -export * from './filter-operand-model'; -export { createApiModels } from './utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts deleted file mode 100644 index 0d0b7f73..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ApiParam, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { ParseIntPipe, Type } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { Binding, Entity, ConfigParam } from '../../../types'; -import { errorSchema } from '../utils'; - -export function deleteOne( - controller: Type, - repository: Repository, - binding: Binding<'deleteOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiOperation({ - summary: `Delete item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: `Item of resource "${entityName}" not found`, - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 204, - description: `Item of resource "${entityName}" has been deleted`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong query parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts deleted file mode 100644 index f8568432..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { ParseIntPipe, Type } from '@nestjs/common'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { createZodDto } from '@anatine/zod-nestjs'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { Repository } from 'typeorm'; - -import { Binding, ConfigParam, Entity } from '../../../types'; -import { errorSchema } from '../utils'; -import { getField } from '../../orm'; -import { zodInputPatchRelationshipSchema, zodInputPostSchema } from '../../zod'; - -export function deleteRelationship( - controller: Type, - repository: Repository, - binding: Binding<'deleteRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor( - controller.constructor.prototype, - binding.name - ); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}DeleteRelationship`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Delete list of relation for resource "${entityName}"`, - operationId: `${controller.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for delete "${entityName}" item`, - schema: generateSchema(zodInputPatchRelationshipSchema) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 204, - description: `Item/s of relation for "${entityName}" has been deleted`, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts deleted file mode 100644 index c5506eba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { Type } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../constants'; -import { Binding, Entity, ConfigParam } from '../../../types'; -import { ObjectTyped } from '../../utils'; -import { errorSchema, jsonSchemaResponse } from '../utils'; - -import { - fromRelationTreeToArrayName, - getField, - getPrimaryColumnsForRelation, - getPropsTreeForRepository, -} from '../../orm'; - -export function getAll( - controller: Type, - repository: Repository, - binding: Binding<'getAll'>, - config: ConfigParam -): void { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations, field } = getField(repository); - const propsTree = getPropsTreeForRepository(repository); - const relationTree = fromRelationTreeToArrayName(propsTree); - const relationPrimaryColum = getPrimaryColumnsForRelation(repository); - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - ApiOperation({ - summary: `Get list items of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'fields', - required: false, - style: 'deepObject', - schema: { - type: 'object', - }, - examples: { - allField: { - summary: 'Select all field', - description: 'Select field for target and relation', - value: { - target: field.join(','), - ...ObjectTyped.entries(propsTree).reduce((acum, [name, props]) => { - acum[name.toString()] = props.join(','); - return acum; - }, {} as Record), - }, - }, - selectOnlyIdsTarget: { - summary: 'Select ids for target', - description: 'Select ids for target', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - }, - }, - selectOnlyIds: { - summary: 'Select ids', - description: 'Select ids', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - ...ObjectTyped.entries(relationPrimaryColum).reduce( - (acum, [name, props]) => { - acum[name.toString()] = props; - return acum; - }, - {} as Record - ), - }, - }, - }, - description: `Object of field for select field from "${entityName}" resource`, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'filter', - required: false, - style: 'deepObject', - schema: { - type: 'object', - }, - examples: { - simpleExample: { - summary: 'Several conditional', - description: 'Get if relation is not null', - value: { - [field[0]]: { - in: '1,2,3', - }, - [field[1]]: { - lt: '1', - }, - [relationTree[0]]: { - eq: 'test', - }, - }, - }, - relationNull: { - summary: 'Get if relation is null', - description: 'Get if relation is null', - value: { - [relations[0]]: { - eq: null, - }, - }, - }, - relationNotNull: { - summary: 'Get if relation is not null', - description: 'Get if relation is not null', - value: { - [relations[0]]: { - ne: null, - }, - }, - }, - getRelationByConditional: { - summary: 'Get if relation field is', - description: 'Get if relation field is', - value: { - [relationTree[0]]: { - eq: 'test', - }, - }, - }, - }, - description: `Object of filter for select items from "${entityName}" resource`, - })(controller, binding.name, descriptor); - ApiQuery({ - name: 'include', - required: false, - enum: relations, - style: 'simple', - isArray: true, - description: `"${entityName}" resource item has been extended with existing relations`, - examples: { - withInclude: { - summary: 'Add all relation', - description: 'Add all realtion', - value: relations, - }, - without: { - summary: 'Without relation', - description: 'Without all realtion', - value: [], - }, - }, - })(controller, binding.name, descriptor); - ApiQuery({ - name: 'sort', - type: 'string', - required: false, - description: `Params for sorting of "${entityName}"`, - examples: { - sortAsc: { - summary: 'Sort field by ASC', - description: 'Sort field by ASC', - value: field[1], - }, - sortDesc: { - summary: 'Sort field by DESC', - description: 'Sort field by DESC', - value: `-${field[1]}`, - }, - sortAscRelation: { - summary: 'Sort field relation by ASC', - description: 'Sort field relation by ASC', - value: relationTree[2], - }, - sortDescRelation: { - summary: 'Sort field relation by DESC', - description: 'Sort field relation by DESC', - value: `-${relationTree[2]}`, - }, - sortSeveral: { - summary: 'Sort several field relation', - description: 'Sort several field relation', - value: `${field[1]},-${relationTree[2]},${relationTree[1]},-${field[0]}`, - }, - }, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'page', - style: 'deepObject', - required: false, - schema: { - type: 'object', - properties: { - number: { - type: 'integer', - minimum: 1, - example: DEFAULT_QUERY_PAGE, - }, - size: { - type: 'integer', - minimum: 1, - example: DEFAULT_PAGE_SIZE, - maximum: 500, - }, - }, - additionalProperties: false, - }, - description: `"${entityName}" resource has been limit and offset with this params.`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: `Item of resource "${entityName}" not found`, - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong query parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - description: 'Resource list received successfully', - schema: jsonSchemaResponse(repository, true), - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts deleted file mode 100644 index 765238fe..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { Repository } from 'typeorm'; - -import { Binding, Entity, ConfigParam } from '../../../types'; -import { jsonSchemaResponse, errorSchema } from '../utils'; - -import { - getField, - getPropsTreeForRepository, - getPrimaryColumnsForRelation, -} from '../../orm'; -import { ObjectTyped } from '../../utils'; - -export function getOne( - controller: Type, - repository: Repository, - binding: Binding<'getOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - if (!descriptor) - throw new Error( - `Descriptor for entity controller ${entityName}:${binding.name} is empty` - ); - - const { relations, field } = getField(repository); - const propsTree = getPropsTreeForRepository(repository); - const relationPrimaryColum = getPrimaryColumnsForRelation(repository); - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - ApiOperation({ - summary: `Get one item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'fields', - required: false, - style: 'deepObject', - schema: { - type: 'object', - }, - examples: { - allField: { - summary: 'Select all field', - description: 'Select field for target and relation', - value: { - target: field.join(','), - ...ObjectTyped.entries(propsTree).reduce((acum, [name, props]) => { - acum[name.toString()] = props.join(','); - return acum; - }, {} as Record), - }, - }, - selectOnlyIdsTarget: { - summary: 'Select ids for target', - description: 'Select ids for target', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - }, - }, - selectOnlyIds: { - summary: 'Select ids', - description: 'Select ids', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - ...ObjectTyped.entries(relationPrimaryColum).reduce( - (acum, [name, props]) => { - acum[name.toString()] = props; - return acum; - }, - {} as Record - ), - }, - }, - }, - description: `Object of field for select field from "${entityName}" resource`, - })(controller, binding.name, descriptor); - ApiQuery({ - name: 'include', - required: false, - enum: relations, - style: 'simple', - isArray: true, - description: `"${entityName}" resource item has been extended with existing relations`, - examples: { - withInclude: { - summary: 'Add all relation', - description: 'Add all realtion', - value: relations, - }, - without: { - summary: 'Without relation', - description: 'Without all realtion', - value: [], - }, - }, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: `Item of resource "${entityName}" not found`, - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong query parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - description: 'Resource one item received successfully', - schema: jsonSchemaResponse(repository), - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts deleted file mode 100644 index 31d817f4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; -import { Repository } from 'typeorm'; - -import { Binding, ConfigParam, Entity } from '../../../types'; -import { errorSchema, schemaTypeForRelation } from '../utils'; -import { getField } from '../../orm'; - -export function getRelationship( - controller: Type, - repository: Repository, - binding: Binding<'getRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - ApiOperation({ - summary: `Get list of relation for resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - schema: schemaTypeForRelation, - description: `Item/s of relation for "${entityName}" has been created`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts deleted file mode 100644 index 232f6eff..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getAll } from './get-all'; -import { getOne } from './get-one'; -import { deleteOne } from './delete-one'; -import { postOne } from './post-one'; -import { patchOne } from './patch-one'; -import { getRelationship } from './get-relationship'; -import { deleteRelationship } from './delete-relationship'; -import { postRelationship } from './post-relationship'; -import { patchRelationship } from './patch-relationship'; -import { MethodName } from '../../../types'; - -export type SwaggerMethod = { - [Key in MethodName]?: any; -}; - -export const swaggerMethod: SwaggerMethod = { - getAll, - getOne, - deleteOne, - postOne, - patchOne, - getRelationship, - deleteRelationship, - postRelationship, - patchRelationship, -}; - -export const errorSchema = { - type: 'object', - properties: { - status: { - type: 'number', - }, - errors: { - type: 'array', - items: { - type: 'object', - properties: { - detail: { - type: 'string', - }, - source: { - type: 'object', - properties: { - parameter: { - type: 'string', - }, - }, - }, - }, - required: ['detail'], - }, - }, - }, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts deleted file mode 100644 index a4abf4aa..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { ParseIntPipe, Type } from '@nestjs/common'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { Repository } from 'typeorm'; - -import { jsonSchemaResponse, errorSchema } from '../utils'; - -import { zodInputPatchSchema, zodInputPostSchema } from '../../zod'; -import { Binding, ConfigParam, Entity } from '../../../types'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function patchOne( - controller: Type, - repository: Repository, - binding: Binding<'patchOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PatchOne`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Update item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for update "${entityName}" item`, - schema: generateSchema(zodInputPatchSchema(repository)) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - description: `Item of resource "${entityName}" has been updated`, - schema: jsonSchemaResponse(repository), - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong body parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Unprocessable data', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts deleted file mode 100644 index b87b2cfa..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { Repository } from 'typeorm'; -import { Binding, ConfigParam, Entity } from '../../../types'; -import { schemaTypeForRelation, errorSchema } from '../utils'; -import { zodInputPatchRelationshipSchema, zodInputPostSchema } from '../../zod'; -import { getField } from '../../orm'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function patchRelationship( - controller: Type, - repository: Repository, - binding: Binding<'patchRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PatchRelationship`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Update list of relation for resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller.prototype, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller.prototype, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for update "${entityName}" item`, - schema: generateSchema(zodInputPatchRelationshipSchema) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 200, - schema: schemaTypeForRelation, - description: `Item/s of relation for "${entityName}" has been updated`, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller.prototype, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts deleted file mode 100644 index 608e0fae..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - ApiOperation, - ApiResponse, - ApiBody, - ApiExtraModels, -} from '@nestjs/swagger'; - -import { Binding, ConfigParam, Entity } from '../../../types'; -import { jsonSchemaResponse } from '../utils'; - -import { errorSchema } from '../utils'; -import { Type } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { generateSchema, extendApi } from '@anatine/zod-openapi'; - -import { zodInputPostSchema } from '../../zod'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function postOne( - controller: Type, - repository: Repository, - binding: Binding<'postOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PostOne`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Create item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for new "${entityName}" item`, - schema: generateSchema(zodInputPostSchema(repository)) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 201, - description: `Item of resource "${entityName}" has been created`, - schema: jsonSchemaResponse(repository), - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong body parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Unprocessable data', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts deleted file mode 100644 index 6cdeb525..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { Repository } from 'typeorm'; - -import { getField } from '../../orm'; -import { zodInputPostRelationshipSchema, zodInputPostSchema } from '../../zod'; -import { Binding, ConfigParam, Entity } from '../../../types'; -import { schemaTypeForRelation, errorSchema } from '../utils'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function postRelationship( - controller: Type, - repository: Repository, - binding: Binding<'postRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PostRelationship`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for update "${entityName}" item`, - schema: generateSchema(zodInputPostRelationshipSchema) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiOperation({ - summary: `Create list of relation for resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - schema: schemaTypeForRelation, - description: `Item/s of relation for "${entityName}" has been created`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts deleted file mode 100644 index 58b91728..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { Type } from '@nestjs/common'; -import { ApiProperty } from '@nestjs/swagger'; -import { DeepPartial } from 'typeorm/common/DeepPartial'; -import { Repository } from 'typeorm'; - -import { - getField, - getFieldWithType, - TypeField, - getIsArrayRelation, - getRelationTypeName, -} from '../orm'; -import { camelToKebab, nameIt, ObjectTyped } from '../utils'; -import { Entity, EntityRelation } from '../../types'; - -export const errorSchema = { - type: 'object', - properties: { - statusCode: { - type: 'number', - }, - error: { - type: 'string', - }, - message: { - type: 'array', - items: { - type: 'object', - properties: { - code: { - type: 'string', - }, - message: { - type: 'string', - }, - path: { - type: 'array', - items: { - type: 'string', - }, - }, - keys: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - required: ['code', 'message', 'path'], - }, - }, - }, -}; - -export function jsonSchemaResponse( - repository: Repository, - array = false -) { - const { relations } = getField(repository); - const fieldTypes = getFieldWithType(repository); - const arrayField = getIsArrayRelation(repository); - const relationTypeName = getRelationTypeName(repository); - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - const dataType = { - type: 'object', - properties: { - type: { - type: 'string', - enum: [camelToKebab(repository.metadata.name)], - }, - id: { - type: 'string', - }, - attributes: { - type: 'object', - properties: ObjectTyped.entries(fieldTypes) - .filter(([name]) => name !== primaryColumn) - .reduce((acum, [name, type]) => { - switch (type) { - case TypeField.array: - acum[name.toString()] = { - type: 'array', - items: { - type: 'string', - }, - }; - break; - case TypeField.date: - acum[name.toString()] = { - format: 'date-time', - type: 'string', - }; - break; - case TypeField.number: - acum[name.toString()] = { - type: 'integer', - }; - break; - case TypeField.boolean: - acum[name.toString()] = { - type: 'boolean', - }; - break; - default: - acum[name.toString()] = { - type: 'string', - }; - } - return acum; - }, {} as Record), - }, - relationships: { - type: 'object', - properties: relations.reduce((acum, name) => { - const dataItem = { - type: 'object', - properties: { - type: { - type: 'string', - enum: [ - camelToKebab(relationTypeName[name as EntityRelation]), - ], - }, - id: { - type: 'string', - }, - }, - required: ['type', 'id'], - }; - const dataArray = { - type: 'array', - items: dataItem, - }; - acum[name.toString()] = { - type: 'object', - properties: { - links: { - type: 'object', - properties: { - self: { - type: 'string', - }, - }, - required: ['self'], - }, - data: arrayField[name as EntityRelation] - ? dataArray - : dataItem, - }, - required: ['links'], - }; - return acum; - }, {} as Record), - }, - links: { - type: 'object', - properties: { - self: { - type: 'string', - }, - }, - required: ['self'], - }, - }, - }; - const dataTypeArra = { - type: 'array', - items: dataType, - }; - return { - type: 'object', - properties: { - meta: { - type: 'object', - }, - data: array ? dataTypeArra : dataType, - includes: { - type: 'array', - items: { - type: 'object', - properties: { - type: { - type: 'string', - }, - id: { - type: 'string', - }, - attributes: { - type: 'object', - }, - relationships: { - type: 'object', - properties: { - relationName: { - properties: { - links: { - type: 'object', - properties: { - self: { - type: 'string', - }, - }, - required: ['self'], - }, - }, - required: ['links'], - }, - }, - }, - links: { - type: 'object', - properties: { - self: { - type: 'string', - }, - }, - required: ['self'], - }, - }, - required: ['type', 'id', 'attributes'], - }, - }, - }, - required: ['meta', 'data'], - }; -} - -export function createApiModels( - repository: Repository -): Type { - const propsType = getFieldWithType(repository); - const relationTypeName = getRelationTypeName(repository); - const relationArray = getIsArrayRelation(repository); - - const result = repository.create({ - ...propsType, - ...ObjectTyped.entries(relationTypeName).reduce((acum, [name, value]) => { - acum[name.toString()] = relationArray[name] ? [value] : value; - return acum; - }, {} as any), - } as DeepPartial); - - const newEntity = nameIt(repository.metadata.name, class {}) as Type; - for (const [name, value] of Object.entries(result)) { - let currentType: any; - let isArray = false; - if (name in propsType) { - switch (value) { - case TypeField.array: - currentType = String; - isArray = true; - break; - case TypeField.date: - currentType = Date; - break; - case TypeField.number: - currentType = Number; - break; - case TypeField.boolean: - currentType = Boolean; - break; - default: - currentType = String; - } - } else { - currentType = relationTypeName[name as EntityRelation]; - isArray = Array.isArray(value); - } - ApiProperty({ - required: false, - isArray: isArray, - type: () => currentType, - })(newEntity.prototype, name); - } - - return newEntity; -} - -const dataType = { - type: 'object', - properties: { - type: { - type: 'string', - }, - id: { - type: 'string', - }, - }, -}; -export const schemaTypeForRelation = { - type: 'object', - properties: { - data: { - oneOf: [ - dataType, - { type: 'null' }, - { - type: 'array', - items: dataType, - }, - ], - }, - }, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts deleted file mode 100644 index fd1d708e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getEntityName, nameIt } from './'; - -describe('Test utils', () => { - it('getEntityName', () => { - expect(getEntityName('Entity')).toBe('Entity'); - expect(getEntityName(class EntityClass {})).toBe('EntityClass'); - class EntityClassInst {} - const tmp = new EntityClassInst(); - expect(getEntityName(tmp as any)).toBe('EntityClassInst'); - }); - - it('nameIt', () => { - const newNameClass = 'newNameClass'; - const newClass = nameIt(newNameClass, class {}); - expect(getEntityName(newClass)).toBe(newNameClass); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/utils.ts deleted file mode 100644 index 1ac82eee..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { EntityTarget } from 'typeorm/common/EntityTarget'; -import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; -import { Type } from '@nestjs/common/interfaces'; -import { JSON_API_DECORATOR_ENTITY } from '../constants'; - -import { Entity } from '../types'; - -import { upperFirstLetter } from 'shared-utils'; - -export { - camelToKebab, - snakeToCamel, - kebabToCamel, - upperFirstLetter, - isString, - ObjectTyped, -} from 'shared-utils'; - -export const nameIt = ( - name: string, - cls: new (...rest: unknown[]) => Record -) => - ({ - [name]: class extends cls { - constructor(...arg: unknown[]) { - super(...arg); - } - }, - }[name]); - -export const getEntityName = ( - entity: EntityTarget -): string => { - if (typeof entity === 'string') { - return entity; - } - - if ('name' in entity) { - return entity['name']; - } - - if ('constructor' in entity && 'name' in entity.constructor) { - return entity['constructor']['name']; - } - - return `${entity}`; -}; - -export function getProviderName(entity: EntityTarget, name: string) { - const entityName = getEntityName(entity); - return `${upperFirstLetter(entityName)}${name}`; -} - -export function entityForClass(type: Type): Entity { - return Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, type); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts deleted file mode 100644 index b70e311c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './zod-helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts deleted file mode 100644 index 3221b9bc..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts +++ /dev/null @@ -1,728 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { IMemoryDb } from 'pg-mem'; -import { z, ZodError } from 'zod'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - pullUser, - Roles, - UserGroups, - Users, -} from '../../mock-utils'; -import { - QueryField, - zodInputQuerySchema, - ZodInputQuerySchema, - zodQuerySchema, - ZodQuerySchema, - zodInputPostSchema, - ZodInputPostSchema, - ZodInputPatchSchema, - zodInputPatchSchema, - zodInputPostRelationshipSchema, - zodInputPatchRelationshipSchema, -} from './zod-helper'; -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../constants'; - -const page = { - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, -}; - -describe('zod-helper', () => { - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - let db: IMemoryDb; - let zodInputQuerySchemaTest: ZodInputQuerySchema; - type zodInputQuerySchema = z.infer; - type TypeFilterInputQuery = zodInputQuerySchema['filter']; - type TypeIncludeInputQuery = zodInputQuerySchema['include']; - type TypeFieldsInputQuery = zodInputQuerySchema['fields']; - type TypePageInputQuery = zodInputQuerySchema['page']; - - let zodQuerySchemaTest: ZodQuerySchema; - type zodQuerySchema = z.infer; - type TypeFilterQuery = zodQuerySchema['filter']; - type TypeIncludeQuery = zodQuerySchema['include']; - type TypeFieldsQuery = zodQuerySchema['fields']; - type TypePageQuery = zodQuerySchema['page']; - type TypeSortQuery = zodQuerySchema['sort']; - const dataCheckDefault = { - [QueryField.filter]: null, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: page[QueryField.page], - }; - - let zodInputPostSchemaTest: ZodInputPostSchema; - type zodInputPostSchema = z.infer; - - let zodInputPatchSchemaTest: ZodInputPatchSchema; - type zodInputPatchSchema = z.infer; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [...providerEntities(getDataSourceToken())], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - - const user = await pullUser(userRepository); - const userWithRelation = await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - zodInputQuerySchemaTest = zodInputQuerySchema(userRepository); - zodQuerySchemaTest = zodQuerySchema(userRepository); - zodInputPostSchemaTest = zodInputPostSchema(userRepository); - zodInputPatchSchemaTest = zodInputPatchSchema(userRepository); - }); - - describe('Test input query schema', () => { - it('Empty object is correct', () => { - const check = {}; - const result = zodInputQuerySchemaTest.parse(check); - expect(result).toEqual({ - ...check, - ...page, - }); - }); - - describe('Test filter', () => { - it('Valid schema', () => { - const check1: TypeFilterInputQuery = { - id: '1', - firstName: { - ne: 'dfs', - eq: 'sdf', - }, - isActive: { - in: 'sdf', - }, - }; - const check2: TypeFilterInputQuery = { - addresses: { - eq: 'null', - }, - manager: { - ne: 'null', - }, - }; - - const check3: TypeFilterInputQuery = { - 'addresses.createdAt': 'sdfsdf', - 'comments.createdAt': { - eq: 'sdfsd', - ne: 'sdfsdf', - like: 'sdfsdf', - }, - }; - const arrayCheck = [check1, check2, check3]; - - for (const item of arrayCheck) { - const dataCheck: zodInputQuerySchema = { - filter: item, - ...page, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('In Valid schema', () => { - const check1 = {}; - const check2 = { - addresses: { - in: null, - }, - manager: { - like: null, - sdfsdf: 'sdfsdf', - }, - }; - const check3 = { - sdfsdf: 'sdfsdf', - }; - const check4 = { - id: '', - firstName: { - ne: '', - eq: '', - }, - isActive: { - in: '', - }, - }; - const arrayCheck = [check1, check2, check3, check4]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - filter: item, - }; - try { - zodInputQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Test include', () => { - it('Valid schema', () => { - const check1: TypeIncludeInputQuery = 'manager'; - const check2: TypeIncludeInputQuery = 'addresses,roles'; - - const arrayCheck = [check1, check2]; - - for (const item of arrayCheck) { - const dataCheck: zodInputQuerySchema = { - [QueryField.include]: item, - ...page, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('In Valid schema', () => { - const check1 = [] as unknown; - const check2 = ['dfsdfsdf', 'addresses']; - const check3 = ['addresses', 'addresses']; - const check4 = {}; - const arrayCheck = [check1, check2, check3, check4]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - include: item, - }; - try { - zodInputQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Test select field', () => { - it('Valid schema', () => { - const check1: TypeFieldsInputQuery = { - target: 'sdfsdf', - manager: 'dfsdfsdf', - }; - const check2: TypeFieldsInputQuery = { - target: 'sdfsdf', - }; - const check3: TypeFieldsInputQuery = { - addresses: 'sdfsdf', - manager: 'sdfsdf', - }; - - const arrayCheck = [check1, check2, check3]; - - for (const item of arrayCheck) { - const dataCheck: zodInputQuerySchema = { - fields: item, - ...page, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('In Valid schema', () => { - const check1 = [] as unknown[]; - const check2 = { - addresses: 'sdfsdf', - manager: 'sdfsdf', - otherField: 'dsfsdf', - }; - const check3 = { otherField: 'dsfsdf' }; - const check4 = 'sdfsdf'; - const check5 = {}; - const check6 = 'dssd'; - const arrayCheck = [check1, check2, check3, check4, check5, check6]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - fields: item, - }; - try { - zodInputQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Test page field', () => { - it('Valid schema', () => { - const check1 = page['page']; - const check2 = {}; - const check3 = undefined; - - const arrayCheck = [check1, check2, check3]; - - for (const item of arrayCheck) { - const dataCheck = { - [QueryField.page]: item, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(page); - } - }); - }); - }); - - describe('test query schema', () => { - describe('Test filter', () => { - it('Valid schema', () => { - const check1: TypeFilterQuery = { - target: null, - relation: null, - }; - const check2: TypeFilterQuery = { - target: { - id: { - in: ['123'], - }, - }, - relation: { - addresses: { - arrayField: { - some: ['dsfsdf'], - }, - }, - roles: { - id: { - eq: '123', - }, - }, - }, - }; - const arrayCheck = [check1, check2]; - for (const item of arrayCheck) { - const dataCheck = { - ...dataCheckDefault, - [QueryField.filter]: item, - }; - const result = zodQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('Invalid schema', () => { - const check1 = {}; - const check2 = ''; - const check3: unknown[] = []; - const check4 = null; - const check5 = undefined; - const check6 = 1; - const check7 = { - dsfsdf: 'sdf', - }; - const check8 = { - target: null, - }; - const check9 = { - target: {}, - relation: null, - }; - const check10 = { - target: '', - relation: null, - }; - const check11 = { - target: [], - relation: null, - }; - const check12 = { - target: undefined, - relation: null, - }; - const check13 = { - target: 1, - relation: null, - }; - const check14 = { - target: null, - relation: {}, - }; - const check15 = { - target: null, - relation: '', - }; - const check16 = { - target: null, - relation: [], - }; - const check17 = { - target: null, - relation: undefined, - }; - const check18 = { - target: null, - relation: 1, - }; - - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - check17, - check18, - ]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - ...dataCheckDefault, - [QueryField.filter]: item, - }; - try { - zodQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - }); - describe('test zodInputPostSchema', () => { - it('should be ok', () => { - const real = 123.123; - const date = new Date(); - const attributes = { - lastName: 'sdfsdf', - isActive: true, - testDate: date.toISOString(), - testReal: [`${real}`], - testArrayNull: null, - }; - const relationships = { - notes: [ - { - type: 'notes', - id: 'dsfsdf', - }, - ], - }; - const check = { - data: { - type: 'users', - attributes, - relationships, - }, - }; - const check2 = { - data: { - type: 'users', - attributes, - }, - }; - const check3 = { - data: { - id: '1', - type: 'users', - attributes, - }, - }; - - const checkResult = { - data: { - type: 'users', - attributes: { - ...attributes, - ['testDate']: date, - testReal: [real], - }, - relationships, - }, - }; - const checkResult2 = { - data: { - type: 'users', - attributes: { - ...attributes, - ['testDate']: date, - testReal: [real], - }, - }, - }; - const checkResult3 = { - data: { - id: '1', - type: 'users', - attributes: { - ...attributes, - ['testDate']: date, - testReal: [real], - }, - }, - }; - - expect(zodInputPostSchemaTest.parse(check)).toEqual(checkResult); - expect(zodInputPostSchemaTest.parse(check2)).toEqual(checkResult2); - expect(zodInputPostSchemaTest.parse(check3)).toEqual(checkResult3); - }); - - it('should be not ok', () => { - const check1 = {}; - const check2 = null; - const check3: unknown[] = []; - const check4 = ''; - const check5 = { - sdf: 'sdf', - }; - const check6 = { - data: {}, - }; - const check7 = { - data: { - type: 'users', - }, - }; - const check8 = { - data: { - type: 'users', - attributes: { - lastName: 'sdfsdf', - isActive: true, - }, - relationships: { - notes: [ - { - type: 'sdfsdf', - id: 'dsfsdf', - }, - ], - }, - }, - }; - const check9 = { - data: { - type: 'users', - attributes: { - lastName: 'sdfsdf', - id: 1, - }, - }, - }; - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodInputPostSchemaTest.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('test zodInputPatchSchema', () => { - it('should be ok', () => { - const attributes = { - lastName: 'sdfsdf', - isActive: true, - }; - const relationships = { - notes: [], - manager: null, - }; - const check = { - data: { - id: '1', - type: 'users', - attributes, - relationships, - }, - }; - const check2 = { - data: { - id: '1', - type: 'users', - attributes, - }, - }; - const check3 = { - data: { - id: '1', - type: 'users', - relationships, - }, - }; - - expect(zodInputPatchSchemaTest.parse(check)).toEqual(check); - expect(zodInputPatchSchemaTest.parse(check2)).toEqual(check2); - expect(zodInputPatchSchemaTest.parse(check3)).toEqual({ - data: { - id: '1', - type: 'users', - relationships, - attributes: {}, - }, - }); - }); - - it('should be not ok', () => { - const check1 = { - data: { - type: 'users', - attributes: { - lastName: 'sdfsdf', - isActive: true, - }, - }, - }; - const check2 = { - data: { - id: 'sdfsdf', - type: 'users', - attributes: { - lastName: 'sdfsdf', - isActive: true, - }, - }, - }; - const check3 = { - data: { - id: '1', - type: 'users', - attributes: { - id: 1, - isActive: true, - }, - }, - }; - const check4 = { - data: { - id: '1', - type: 'users', - }, - }; - const arrayCheck = [check1, check2, check3, check4]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodInputPatchSchemaTest.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('test zodInputPostRelationshipSchema', () => { - const check = { - data: [{ type: 'type', id: 'id' }], - }; - const check1 = { - data: { type: 'type', id: 'id' }, - }; - it('should be ok', () => { - expect(zodInputPostRelationshipSchema.parse(check)).toEqual(check); - expect(zodInputPostRelationshipSchema.parse(check1)).toEqual(check1); - }); - it('should be not ok', () => { - const check = {}; - const check1 = { - data: { type: 'type', id: 'id' }, - tes: 'sdfsdf', - }; - expect.assertions(2); - try { - zodInputPostRelationshipSchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - try { - zodInputPostRelationshipSchema.parse(check1); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); - }); - - describe('test zodInputPatchRelationshipSchema', () => { - const check = { - data: [], - }; - const check1 = { - data: null, - }; - it('should be ok', () => { - expect(zodInputPatchRelationshipSchema.parse(check)).toEqual(check); - expect(zodInputPatchRelationshipSchema.parse(check1)).toEqual(check1); - }); - it('should be not ok', () => { - const check = {}; - const check1 = { - data: { type: 'type', id: 'id' }, - tes: 'sdfsdf', - }; - expect.assertions(2); - try { - zodInputPatchRelationshipSchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - try { - zodInputPatchRelationshipSchema.parse(check1); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts deleted file mode 100644 index a2177f1b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { Repository } from 'typeorm'; -import { z, ZodObject, ZodUnion } from 'zod'; -import { QueryField } from 'json-shared-type'; - -import { - getField, - getPropsTreeForRepository, - fromRelationTreeToArrayName, - ResultGetField, - getArrayPropsForEntity, - getRelationTypeArray, - getRelationTypeName, - getRelationTypePrimaryColumn, - getFieldWithType, - getTypePrimaryColumn, - getTypeForAllProps, - FieldWithType, - getPropsFromDb, -} from '../orm'; -import { Entity } from '../../types'; - -import { - ZodInputQueryShape, - zodSortInputQuerySchema, - zodPageInputQuerySchema, - zodIncludeInputQuerySchema, - zodSelectFieldsInputQuerySchema, - zodFilterInputQuerySchema, -} from './zod-input-query-schema'; - -import { - ZodQueryShape, - zodFilterQuerySchema, - zodSelectFieldsQuerySchema, - zodIncludeQuerySchema, - zodSortQuerySchema, - zodPageQuerySchema, -} from './zod-query-schema'; - -import { - PostShape, - zodAttributesSchema, - zodRelationshipsSchema, - zodTypeSchema, -} from './zod-input-post-schema'; - -import { - PatchShape, - PatchShapeDefault, - zodPatchRelationshipsSchema, -} from './zod-input-patch-schema'; - -import { - postRelationshipSchema, - PostRelationshipSchema, -} from './zod-input-post-relationship-schema'; - -import { - patchRelationshipSchema, - PatchRelationshipSchema, -} from './zod-input-patch-relationship-schema'; - -import { camelToKebab, getEntityName, ObjectTyped } from '../utils'; -import { zodIdSchema } from './zod-input-post-schema/id'; - -export { QueryField }; - -export type ZodInputQuerySchema = ZodObject< - ZodInputQueryShape, - 'strict' ->; - -export type InputQuery = z.infer>; - -export type ZodQuerySchema = ZodObject< - ZodQueryShape, - 'strict' ->; - -export type Query = z.infer>; - -export type TypeInputProps< - E extends Entity, - K extends keyof ZodInputQueryShape -> = z.infer[K]>; - -export type ZodInputPostSchema = ZodObject< - { - data: ZodObject, 'strict'>; - }, - 'strict' ->; - -export type PostData = z.infer>['data']; - -export type ZodInputPatchSchema = ZodObject< - { - data: ZodUnion< - [ - ZodObject, 'strict'>, - ZodObject, 'strict'> - ] - >; - }, - 'strict' ->; - -export type PatchData = z.infer< - ZodInputPatchSchema ->['data']; - -export type ZodInputPostRelationshipSchema = ZodObject< - { - data: PostRelationshipSchema; - }, - 'strict' ->; - -export type PostRelationshipData = - z.infer['data']; - -export type ZodInputPatchRelationshipSchema = ZodObject< - { - data: PatchRelationshipSchema; - }, - 'strict' ->; - -export type PatchRelationshipData = - z.infer['data']; - -export const zodInputQuerySchema = ( - repository: Repository -): ZodInputQuerySchema => { - const { field, relations } = getField(repository); - const relationTree = fromRelationTreeToArrayName( - getPropsTreeForRepository(repository) - ); - - const zodInputQueryShape: ZodInputQueryShape = { - [QueryField.filter]: zodFilterInputQuerySchema( - field, - relations, - relationTree - ).optional(), - [QueryField.fields]: - zodSelectFieldsInputQuerySchema(relations).optional(), - [QueryField.include]: zodIncludeInputQuerySchema.optional(), - [QueryField.sort]: zodSortInputQuerySchema.optional(), - [QueryField.page]: zodPageInputQuerySchema, - }; - return z - .object(zodInputQueryShape) - .strict( - `Query object should contain only allow params: "${Object.keys( - QueryField - ).join('"."')}"` - ); -}; - -export const zodQuerySchema = ( - repository: Repository -): ZodQuerySchema => { - const { field, relations } = getField(repository); - const relationTree = getPropsTreeForRepository(repository); - const propsArray = getArrayPropsForEntity(repository); - const typeProps = getTypeForAllProps(repository); - - const zodQueryShape: ZodQueryShape = { - [QueryField.filter]: zodFilterQuerySchema( - field, - relationTree, - propsArray, - typeProps - ), - [QueryField.fields]: zodSelectFieldsQuerySchema( - field, - relationTree - ).nullable(), - [QueryField.include]: - zodIncludeQuerySchema['relations']>( - relations - ).nullable(), - [QueryField.sort]: zodSortQuerySchema(field, relationTree).nullable(), - [QueryField.page]: zodPageQuerySchema, - }; - - return z.object(zodQueryShape).strict(); -}; - -export const zodInputPostSchema = ( - repository: Repository -): ZodInputPostSchema => { - const relationArrayProps = getRelationTypeArray(repository); - const relationPopsName = getRelationTypeName(repository); - const primaryColumnType = getRelationTypePrimaryColumn(repository); - const primaryType = getTypePrimaryColumn(repository); - const fieldWithType = ObjectTyped.entries(getFieldWithType(repository)) - .filter( - ([key]) => key !== repository.metadata.primaryColumns[0].propertyName - ) - .reduce( - (acum, [key, type]) => ({ - ...acum, - [key]: type, - }), - {} as FieldWithType - ); - const typeName = camelToKebab(getEntityName(repository.target)); - - const propsDb = getPropsFromDb(repository); - - const postShape: PostShape = { - id: zodIdSchema(primaryType).optional(), - type: zodTypeSchema(typeName), - attributes: zodAttributesSchema(fieldWithType, propsDb), - relationships: zodRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ).optional(), - }; - - return z - .object({ - data: z.object(postShape).strict(), - }) - .strict(); -}; - -export const zodInputPatchSchema = ( - repository: Repository -): ZodInputPatchSchema => { - const relationArrayProps = getRelationTypeArray(repository); - const relationPopsName = getRelationTypeName(repository); - const primaryColumnType = getRelationTypePrimaryColumn(repository); - const primaryType = getTypePrimaryColumn(repository); - - const fieldWithType = ObjectTyped.entries(getFieldWithType(repository)) - .filter( - ([key]) => key !== repository.metadata.primaryColumns[0].propertyName - ) - .reduce( - (acum, [key, type]) => ({ - ...acum, - [key]: type, - }), - {} as FieldWithType - ); - const typeName = camelToKebab(getEntityName(repository.target)); - const propsDb = getPropsFromDb(repository); - - const patchShapeDefault: PatchShapeDefault = { - id: zodIdSchema(primaryType), - type: zodTypeSchema(typeName), - attributes: zodAttributesSchema(fieldWithType, propsDb), - relationships: zodPatchRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ).optional(), - }; - - const patchShape: PatchShape = { - id: zodIdSchema(primaryType), - type: zodTypeSchema(typeName), - attributes: zodAttributesSchema(fieldWithType, propsDb) - .optional() - .default({} as any), - relationships: zodPatchRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ), - }; - - return z - .object({ - data: z.union([ - z.object(patchShapeDefault).strict(), - z.object(patchShape).strict(), - ]), - }) - .strict(); -}; - -export const zodInputPostRelationshipSchema: ZodInputPostRelationshipSchema = z - .object({ - data: postRelationshipSchema, - }) - .strict(); - -export const zodInputPatchRelationshipSchema: ZodInputPatchRelationshipSchema = - z - .object({ - data: patchRelationshipSchema, - }) - .strict(); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts deleted file mode 100644 index a3cd25bd..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { patchRelationshipSchema, PatchRelationshipSchema } from './'; -import { ZodError } from 'zod'; - -describe('zod-input-patch-relationship-schema', () => { - it('should be ok', () => { - const check = { - type: 'type', - id: 'id', - }; - const check1 = [ - { - type: 'type', - id: 'id', - }, - ]; - const check2 = null; - const check3: any[] = []; - expect(patchRelationshipSchema.parse(check)).toEqual(check); - expect(patchRelationshipSchema.parse(check1)).toEqual(check1); - expect(patchRelationshipSchema.parse(check2)).toEqual(check2); - expect(patchRelationshipSchema.parse(check3)).toEqual(check3); - }); - it('should be not ok', () => { - const check = { - asd: 'sdfsdf', - }; - - const check2 = { - sdfs: 'dsfsdf', - type: 'type', - id: 'id', - }; - const check4 = true; - const check5 = 'dsfsdf'; - - const checkArray = [check, check2, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - patchRelationshipSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts deleted file mode 100644 index ae69387e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z, ZodArray, ZodNullable, ZodUnion } from 'zod'; - -import { Data, data } from '../zod-input-post-relationship-schema'; - -export type PatchRelationshipSchema = ZodUnion< - [ZodNullable, ZodArray] ->; -export const patchRelationshipSchema: PatchRelationshipSchema = z.union([ - data.nullable(), - z.array(data), -]); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts deleted file mode 100644 index 6ce1600b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Entity } from '../../../types'; -import { ZodAttributesSchema } from '../zod-input-post-schema/attributes'; -import { ZodTypeSchema } from '../zod-input-post-schema/type'; -import { ZodIdSchema } from '../zod-input-post-schema/id'; - -import { ZodDefault, ZodObject, ZodOptional } from 'zod'; -import { - ZodPatchRelationshipsSchema, - zodPatchRelationshipsSchema, -} from './relationships'; - -export type PatchShape = { - id: ZodIdSchema; - type: ZodTypeSchema; - attributes: ZodDefault>>; - relationships: ZodPatchRelationshipsSchema; -}; - -export type PatchShapeDefault = { - id: ZodIdSchema; - type: ZodTypeSchema; - attributes: ZodAttributesSchema; - relationships: ZodOptional>; -}; - -export type ZodPatchData = ZodObject, 'strict'>; -export { zodPatchRelationshipsSchema }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts deleted file mode 100644 index d04c1f23..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { - zodPatchRelationshipsSchema, - ZodPatchRelationshipsSchema, -} from './relationships'; -import { Users } from '../../../mock-utils'; -import { - RelationPropsType, - RelationPropsTypeName, - RelationPrimaryColumnType, - TypeField, -} from '../../orm'; - -describe('zodRelationshipsSchema', () => { - const relationArrayProps: RelationPropsType = { - roles: true, - userGroup: false, - notes: true, - addresses: false, - comments: true, - manager: false, - }; - const relationPopsName: RelationPropsTypeName = { - roles: 'Roles', - userGroup: 'UserGroups', - notes: 'Notes', - addresses: 'Addresses', - comments: 'Comments', - manager: 'Users', - }; - - const primaryColumnType: RelationPrimaryColumnType = { - roles: TypeField.number, - userGroup: TypeField.number, - notes: TypeField.string, - addresses: TypeField.number, - comments: TypeField.number, - manager: TypeField.number, - }; - - let relationshipsSchema: ZodPatchRelationshipsSchema; - beforeAll(() => { - relationshipsSchema = zodPatchRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ); - }); - - it('Should be ok', () => { - const check = { - comments: [ - { - type: 'comments', - id: '1', - }, - ], - userGroup: { - type: 'user-groups', - id: '1', - }, - manager: { - type: 'users', - id: '1', - }, - notes: [ - { - type: 'notes', - id: 'id', - }, - ], - }; - const check2 = { - comments: [], - manager: null, - }; - const check3 = { - comments: { - data: [ - { - type: 'comments', - id: '1', - }, - ], - }, - userGroup: { - data: { - type: 'user-groups', - id: '1', - }, - }, - manager: { - data: { - type: 'users', - id: '1', - }, - }, - notes: { - data: [ - { - type: 'notes', - id: 'id', - }, - ], - }, - }; - - expect(relationshipsSchema.parse(check)).toEqual(check); - expect(relationshipsSchema.parse(check2)).toEqual(check2); - expect(relationshipsSchema.parse(check3)).toEqual(check); - }); - - it('should be not ok', () => { - const check1 = {}; - const check2 = ''; - const check3: any[] = []; - const check4 = true; - const check5 = { - sddsf: {}, - }; - const check6 = { - comments: null, - }; - const check7 = { - comments: {}, - }; - const check8 = { - comments: '', - }; - const check9 = { - comments: true, - }; - const check10 = { - comments: [ - { - sdsf: 'sdfsdf', - }, - ], - }; - const check11 = { - comments: [{}], - }; - const check12 = { - manager: {}, - }; - const check13 = { - manager: { - sdfs: 'sdsdf', - }, - }; - const check14 = { - manager: { - id: 'sdsdf', - type: 'users', - }, - }; - const check15 = { - manager: [], - }; - const check16 = { - comments: null, - }; - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - relationshipsSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts deleted file mode 100644 index 4e9e48dd..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - ZodArray, - ZodEffects, - ZodObject, - ZodOptional, - z, - ZodNullable, -} from 'zod'; -import { Entity, EntityRelation } from '../../../types'; -import { - RelationPrimaryColumnType, - RelationPropsType, - RelationPropsTypeName, -} from '../../orm'; - -import { zodDataSchema, ZodDataSchema } from '../zod-input-post-schema/data'; -import { nonEmptyObject } from '../zod-utils'; -import { ObjectTyped, camelToKebab } from '../../utils'; - -export type DataArray = ZodArray>; - -export type DataItem = ZodOptional< - E extends true ? DataArray : ZodNullable> ->; - -export type ShapeRelationships = { - [K in keyof RelationPropsType]: DataItem[K]>; -}; - -export type ZodPatchRelationshipsSchema = ZodEffects< - ZodObject, 'strict'> ->; -export const zodPatchRelationshipsSchema = ( - relationArrayProps: RelationPropsType, - relationPopsName: RelationPropsTypeName, - primaryColumnType: RelationPrimaryColumnType -): ZodPatchRelationshipsSchema => { - const shape = ObjectTyped.entries(relationArrayProps).reduce( - (acum, [props, value]: [EntityRelation, boolean]) => { - const typeName = camelToKebab(relationPopsName[props]); - const primaryType = primaryColumnType[props]; - const zodDataSchemaObject = zodDataSchema(typeName, primaryType); - const dataItem: DataItem = ( - value ? z.array(zodDataSchemaObject) : zodDataSchemaObject.nullable() - ).optional(); - return { - ...acum, - [props]: z.union([ - dataItem, - z - .object({ data: dataItem }) - .strict() - .refine(nonEmptyObject()) - .transform((i) => { - const { data } = i; - return data; - }), - ]), - }; - }, - {} as ShapeRelationships - ); - return z.object(shape).strict().refine(nonEmptyObject()); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts deleted file mode 100644 index 3fe1aec4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { postRelationshipSchema, PostRelationshipSchema } from './'; -import { ZodError } from 'zod'; - -describe('zod-input-post-relationship-schema', () => { - it('should be ok', () => { - const check = { - type: 'type', - id: 'id', - }; - const check1 = [ - { - type: 'type', - id: 'id', - }, - ]; - expect(postRelationshipSchema.parse(check)).toEqual(check); - expect(postRelationshipSchema.parse(check1)).toEqual(check1); - }); - it('should be not ok', () => { - const check = { - asd: 'sdfsdf', - }; - const check1: any[] = []; - const check2 = { - sdfs: 'dsfsdf', - type: 'type', - id: 'id', - }; - const check3 = null; - const check4 = true; - const check5 = 'dsfsdf'; - - const checkArray = [check, check1, check2, check3, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - postRelationshipSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts deleted file mode 100644 index 0386b8b1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { z, ZodArray, ZodObject, ZodString, ZodUnion } from 'zod'; - -export type Data = ZodObject< - { - type: ZodString; - id: ZodString; - }, - 'strict' ->; -export const data: Data = z - .object({ - type: z.string(), - id: z.string(), - }) - .strict(); - -export type PostRelationshipSchema = ZodUnion< - [Data, ZodArray] ->; -export const postRelationshipSchema: PostRelationshipSchema = z.union([ - data, - z.array(data).nonempty(), -]); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts deleted file mode 100644 index 9748a761..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { z, ZodError } from 'zod'; -import { zodAttributesSchema, ZodAttributesSchema } from './attributes'; -import { Addresses, Users } from '../../../mock-utils'; -import { FieldWithType, PropsForField, TypeField } from '../../orm'; - -describe('attributes', () => { - let schemaUsers: ZodAttributesSchema; - let schemaAddresses: ZodAttributesSchema; - type SchemaTypeUsers = z.infer>; - type SchemaTypeAddresses = z.infer>; - beforeAll(() => { - const fieldTypeUsers: FieldWithType = { - id: TypeField.number, - isActive: TypeField.boolean, - firstName: TypeField.string, - createdAt: TypeField.date, - lastName: TypeField.string, - login: TypeField.string, - testDate: TypeField.date, - updatedAt: TypeField.date, - testReal: TypeField.array, - testArrayNull: TypeField.array, - }; - const propsDb: PropsForField = { - id: { type: Number, isArray: false, isNullable: false }, - login: { type: 'varchar', isArray: false, isNullable: false }, - firstName: { type: 'varchar', isArray: false, isNullable: true }, - testReal: { type: 'real', isArray: true, isNullable: false }, - testArrayNull: { type: 'real', isArray: true, isNullable: true }, - lastName: { type: 'varchar', isArray: false, isNullable: true }, - isActive: { type: 'boolean', isArray: false, isNullable: true }, - createdAt: { type: 'timestamp', isArray: false, isNullable: true }, - testDate: { type: 'timestamp', isArray: false, isNullable: true }, - updatedAt: { type: 'timestamp', isArray: false, isNullable: true }, - }; - const fieldTypeAddresses: FieldWithType = { - id: TypeField.number, - arrayField: TypeField.array, - state: TypeField.string, - city: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - country: TypeField.string, - }; - schemaUsers = zodAttributesSchema(fieldTypeUsers, propsDb); - schemaAddresses = zodAttributesSchema( - fieldTypeAddresses, - {} as PropsForField - ); - }); - - it('should be ok', () => { - const check: SchemaTypeUsers = { - isActive: true, - lastName: 'sdsdf', - testReal: [123.123, 123.123], - }; - const check2: SchemaTypeAddresses = { - arrayField: ['test', 'test'], - }; - const date = new Date(); - const check3 = { - testDate: date.toISOString(), - }; - expect(schemaUsers.parse(check)).toEqual(check); - expect(schemaAddresses.parse(check2)).toEqual(check2); - expect(schemaUsers.parse(check3)).toEqual({ - testDate: date, - }); - }); - - it('should be not ok', () => { - const check = { - id: '1', - isActive: 'true', - lastName: 1, - }; - const check2 = { - arrayField: 'test', - }; - expect.assertions(2); - try { - expect(schemaUsers.parse(check)).toEqual(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - try { - schemaAddresses.parse(check2); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts deleted file mode 100644 index 58e0d2c4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { - ZodArray, - ZodBoolean, - ZodEnum, - ZodNumber, - ZodString, - z, - ZodDate, - ZodObject, - ZodEffects, - ZodOptional, - ZodNullable, - ZodType, -} from 'zod'; - -import { Entity } from '../../../types'; -import { - FieldWithType, - PropsFieldItem, - PropsForField, - TypeField, -} from '../../orm'; -import { ObjectTyped } from '../../utils'; -import { nonEmptyObject } from '../zod-utils'; - -const literalSchema = z.union([z.string(), z.number(), z.boolean()]); - -const getZodSchemaForJson = (isNull: boolean) => { - const tmpSchema = isNull ? literalSchema.nullable() : literalSchema; - const jsonSchema: any = z.lazy(() => - z.union([ - tmpSchema, - z.array(jsonSchema.nullable()), - z.record(jsonSchema.nullable()), - ]) - ); - - return jsonSchema; -}; - -type Literal = ReturnType; - -type Json = Literal | { [key: string]: Json } | Json[]; - -type ZodTypeForArray = - | ZodString - | ZodDate - | ZodEffects - | ZodBoolean; -type ZodArrayType = - | ZodArray - | ZodNullable>; - -type TypeMapToZod = { - [TypeField.array]: ZodOptional; - [TypeField.date]: ZodOptional>; - [TypeField.number]: ZodOptional< - | ZodEffects - | ZodNullable> - >; - [TypeField.boolean]: ZodOptional>; - [TypeField.string]: ZodOptional< - | ZodString - | ZodEnum<[string, ...string[]]> - | ZodNullable> - >; - [TypeField.object]: ZodType | ZodNullable>; -}; - -type ZodShapeAttributes = Omit< - { - [K in keyof FieldWithType]: TypeMapToZod[FieldWithType[K]]; - }, - 'id' ->; - -export type ZodAttributesSchema = ZodEffects< - ZodObject, 'strict'> ->; - -function getZodSchemaForArray(props: PropsFieldItem): ZodTypeForArray { - if (!props) return z.string(); - let zodSchema: ZodTypeForArray; - switch (props.type) { - case 'number': - case 'real': - case 'integer': - case 'bigint': - case 'double': - case 'numeric': - case Number: - zodSchema = z.preprocess((x) => Number(x), z.number()); - break; - case 'date': - case Date: - zodSchema = z.coerce.date(); - break; - case 'boolean': - case Boolean: - zodSchema = z.boolean(); - break; - default: - zodSchema = z.string(); - } - - return zodSchema; -} - -export const zodAttributesSchema = ( - fieldWithType: FieldWithType, - propsDb: PropsForField -): ZodAttributesSchema => { - const shape = ObjectTyped.entries(fieldWithType).reduce( - (acum, [props, type]: [keyof FieldWithType, TypeField]) => { - let zodShema: TypeMapToZod[typeof type]; - const propsDbType = propsDb[props]; - switch (type) { - case TypeField.array: { - const tmpSchema = getZodSchemaForArray(propsDbType).array(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.date: { - const tmpSchema = z.coerce.date(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.number: { - const tmpSchema = z.preprocess((x) => Number(x), z.number()); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.boolean: { - const tmpSchema = z.boolean(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.object: { - zodShema = getZodSchemaForJson(propsDbType.isNullable).optional(); - break; - } - case TypeField.string: { - const tmpSchema = z.string(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - } - - return { - ...acum, - [props]: zodShema, - }; - }, - {} as ZodShapeAttributes - ); - - return z.object(shape).strict().refine(nonEmptyObject); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts deleted file mode 100644 index 00f409d7..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { zodDataSchema, ZodDataSchema } from './data'; -import { TypeField } from '../../orm'; -import { ZodError } from 'zod'; - -describe('zodDataSchema', () => { - let zodData: ZodDataSchema; - beforeAll(() => { - zodData = zodDataSchema('users', TypeField.string); - }); - - it('Should be ok', () => { - const check = { - type: 'users', - id: 'id', - }; - expect(zodData.parse(check)).toEqual(check); - }); - - it('Should be not ok', () => { - const check = {}; - const check1 = { - test: '1', - }; - const check3: any[] = []; - const check4 = 'adfsdf'; - const check5 = true; - const checkArray = [check, check1, check3, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - zodData.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts deleted file mode 100644 index 71608126..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z, ZodEffects, ZodObject } from 'zod'; - -import { ZodTypeSchema, zodTypeSchema } from './type'; -import { ZodIdSchema, zodIdSchema } from './id'; -import { TypeForId } from '../../orm'; -import { nonEmptyObject } from '../zod-utils'; - -export type ZodDataSchema = ZodEffects< - ZodObject<{ - id: ZodIdSchema; - type: ZodTypeSchema; - }> ->; -export const zodDataSchema = ( - type: string, - typeId: TypeForId -): ZodDataSchema => - z - .object({ - id: zodIdSchema(typeId), - type: zodTypeSchema(type), - }) - .strict() - .refine(nonEmptyObject); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts deleted file mode 100644 index 801a7c38..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { zodIdSchema, ZodIdSchema } from './id'; -import { TypeField } from '../../orm'; -import { ZodError } from 'zod'; - -describe('zodIdSchema', () => { - let numberStringSchema: ZodIdSchema; - let stringSchema: ZodIdSchema; - beforeAll(() => { - numberStringSchema = zodIdSchema(TypeField.number); - stringSchema = zodIdSchema(TypeField.string); - }); - - it('Should be correct', () => { - const check1 = '1'; - const check2 = '12'; - const check3 = '123'; - const check4 = '-123'; - - const check5 = 'sfdsf'; - const checkArray = [check1, check2, check3, check4]; - for (const item of checkArray) { - expect(numberStringSchema.parse(item)).toBe(item); - } - expect(stringSchema.parse(check5)).toBe(check5); - }); - - it('Should be not ok', () => { - expect.assertions(1); - - try { - numberStringSchema.parse('sdfdfsfsf'); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts deleted file mode 100644 index ef734c03..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z, ZodString } from 'zod'; -import { TypeField, TypeForId } from '../../orm'; - -const reg = new RegExp('^-?\\d+$'); - -export type ZodIdSchema = ZodString; -export const zodIdSchema = (typeId: TypeForId): ZodIdSchema => { - let idSchema = z.string(); - if (typeId === TypeField.number) { - idSchema = idSchema.regex(reg); - } - - return idSchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts deleted file mode 100644 index fb6289e0..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ZodObject, ZodOptional } from 'zod'; - -import { Entity } from '../../../types'; - -import { zodAttributesSchema, ZodAttributesSchema } from './attributes'; -import { zodTypeSchema, ZodTypeSchema } from './type'; -import { - zodRelationshipsSchema, - ZodRelationshipsSchema, -} from './relationships'; -import { ZodIdSchema } from './id'; - -export type PostShape = { - id: ZodOptional; - attributes: ZodAttributesSchema; - type: ZodTypeSchema; - relationships: ZodOptional>; -}; - -export type ZodPostData = ZodObject, 'strict'>; - -export { zodAttributesSchema, zodTypeSchema, zodRelationshipsSchema }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts deleted file mode 100644 index 54604aba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { - zodRelationshipsSchema, - ZodRelationshipsSchema, -} from './relationships'; -import { Users } from '../../../mock-utils'; -import { - RelationPropsType, - RelationPropsTypeName, - RelationPrimaryColumnType, - TypeField, -} from '../../orm'; - -describe('zodRelationshipsSchema', () => { - const relationArrayProps: RelationPropsType = { - roles: true, - userGroup: false, - notes: true, - addresses: false, - comments: true, - manager: false, - }; - const relationPopsName: RelationPropsTypeName = { - roles: 'Roles', - userGroup: 'UserGroups', - notes: 'Notes', - addresses: 'Addresses', - comments: 'Comments', - manager: 'Users', - }; - - const primaryColumnType: RelationPrimaryColumnType = { - roles: TypeField.number, - userGroup: TypeField.number, - notes: TypeField.string, - addresses: TypeField.number, - comments: TypeField.number, - manager: TypeField.number, - }; - - let relationshipsSchema: ZodRelationshipsSchema; - beforeAll(() => { - relationshipsSchema = zodRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ); - }); - - it('Should be ok', () => { - const check = { - comments: [ - { - type: 'comments', - id: '1', - }, - ], - userGroup: { - type: 'user-groups', - id: '1', - }, - manager: { - type: 'users', - id: '1', - }, - notes: [ - { - type: 'notes', - id: 'id', - }, - ], - }; - const check2 = { - comments: { - data: [ - { - type: 'comments', - id: '1', - }, - ], - }, - userGroup: { - data: { - type: 'user-groups', - id: '1', - }, - }, - manager: { - data: { - type: 'users', - id: '1', - }, - }, - notes: { - data: [ - { - type: 'notes', - id: 'id', - }, - ], - }, - }; - expect(relationshipsSchema.parse(check)).toEqual(check); - expect(relationshipsSchema.parse(check2)).toEqual(check); - }); - - it('should be not ok', () => { - const check1 = {}; - const check2 = ''; - const check3: any[] = []; - const check4 = true; - const check5 = { - sddsf: {}, - }; - const check6 = { - comments: [], - }; - const check7 = { - comments: {}, - }; - const check8 = { - comments: '', - }; - const check9 = { - comments: true, - }; - const check10 = { - comments: [ - { - sdsf: 'sdfsdf', - }, - ], - }; - const check11 = { - comments: [{}], - }; - const check12 = { - manager: {}, - }; - const check13 = { - manager: { - sdfs: 'sdsdf', - }, - }; - const check14 = { - manager: { - id: 'sdsdf', - type: 'users', - }, - }; - const check15 = { - manager: null, - }; - const check16 = { - manager: [], - }; - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - relationshipsSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts deleted file mode 100644 index 3cad63e6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ZodArray, ZodEffects, ZodObject, ZodOptional, z } from 'zod'; -import { Entity, EntityRelation } from '../../../types'; -import { - ArrayPropsForEntity, - RelationPrimaryColumnType, - RelationPropsType, - RelationPropsTypeName, -} from '../../orm'; - -import { zodDataSchema, ZodDataSchema } from './data'; -import { nonEmptyObject } from '../zod-utils'; -import { ObjectTyped, camelToKebab } from '../../utils'; - -export type PropsArray = Omit< - ArrayPropsForEntity, - 'target' ->; - -export type DataArray = ZodArray< - ZodDataSchema, - 'atleastone' ->; - -export type DataItem = ZodOptional< - E extends true ? DataArray : ZodDataSchema ->; - -export type ShapeRelationships = { - [K in keyof RelationPropsType]: DataItem[K]>; -}; - -export type ZodRelationshipsSchema = ZodEffects< - ZodObject, 'strict'> ->; -export const zodRelationshipsSchema = ( - relationArrayProps: RelationPropsType, - relationPopsName: RelationPropsTypeName, - primaryColumnType: RelationPrimaryColumnType -): ZodRelationshipsSchema => { - const shape = ObjectTyped.entries(relationArrayProps).reduce( - (acum, [props, value]: [EntityRelation, boolean]) => { - const typeName = camelToKebab(relationPopsName[props]); - const primaryType = primaryColumnType[props]; - const zodDataSchemaObject = zodDataSchema(typeName, primaryType); - const dataItem: DataItem = ( - value ? z.array(zodDataSchemaObject).nonempty() : zodDataSchemaObject - ).optional(); - return { - ...acum, - [props]: z.union([ - dataItem, - z - .object({ data: dataItem }) - .strict() - .refine(nonEmptyObject()) - .transform((i) => { - const { data } = i; - return data; - }), - ]), - }; - }, - {} as ShapeRelationships - ); - - return z.object(shape).strict().refine(nonEmptyObject()); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts deleted file mode 100644 index 9023573c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { zodTypeSchema, ZodTypeSchema } from './type'; -import { ZodError } from 'zod'; - -describe('type', () => { - const literal = 'users'; - let userTypeSchema: ZodTypeSchema; - beforeAll(() => { - userTypeSchema = zodTypeSchema(literal); - }); - it('should be ok', () => { - expect(userTypeSchema.parse(literal)).toEqual(literal); - }); - it('should be ok', () => { - expect.assertions(1); - try { - userTypeSchema.parse('test'); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts deleted file mode 100644 index 84290c0f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { z, ZodLiteral } from 'zod'; - -export type ZodTypeSchema = ZodLiteral; -export const zodTypeSchema = (type: T) => z.literal(type); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts deleted file mode 100644 index bb737d35..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { z, ZodError } from 'zod'; -import { - zodFilterFieldSchema, - zodFilterRelationSchema, - zodFilterSchema, -} from './filter'; - -describe('check "filter"', () => { - describe('Check "zodFilterRelationSchema"', () => { - it('Valid check', () => { - const check1: z.infer = { eq: 'null' }; - const check2: z.infer = { ne: 'null' }; - - const arrayCheck = [check1, check2]; - - for (const item of arrayCheck) { - const result = zodFilterRelationSchema.parse(item); - expect(result).toEqual(item); - } - }); - - it('Invalid check', () => { - const check1 = { eq: null, ne: null }; - const check2 = { eq: 123123 }; - const check3 = { eq: '123123' }; - const check4 = { ne: '123123' }; - const check5 = { ne: true }; - const check6 = { ne: 'true' }; - const check7 = { sdfsdf: 'true' }; - - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodFilterRelationSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Check "zodFilterFieldSchema:', () => { - it('Valid schema', () => { - const check1: z.infer = { - some: 'sdf', - ne: 'sdfsdf', - }; - const check2: z.infer = { - eq: 'sdf', - ne: 'sdfsdf', - in: 'sdsf', - }; - const check3: z.infer = { - like: 'sdsf', - }; - const check4: z.infer = 'dsfsdf'; - const arrayCheck = [check1, check2, check3, check4]; - for (const item of arrayCheck) { - const result = zodFilterFieldSchema.parse(item); - expect(result).toEqual(item); - } - }); - - it('Invalid schema', () => { - const check1 = { dfd: 'dfsdf' }; - const check2 = { dfd: 'dfsdf', like: 'sdsf' }; - const check3 = {}; - const check4 = [] as any[]; - const check5 = null; - const check6 = ''; - - const arrayCheck = [check1, check2, check3, check4, check5, check6]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodFilterFieldSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Check "zodFilterSchema"', () => { - it('Valid schema', () => { - const check1: z.infer = { - some: 'sdf', - ne: 'sdfsdf', - }; - const check2: z.infer = { - eq: 'sdf', - ne: 'sdfsdf', - in: 'sdsf', - }; - const check3: z.infer = { - like: 'sdsf', - }; - const arrayCheck = [check1, check2, check3]; - for (const item of arrayCheck) { - const result = zodFilterSchema.parse(item); - expect(result).toEqual(item); - } - }); - - it('Invalid schema', () => { - const check1 = { dfd: 'dfsdf' }; - const check2 = { dfd: 'dfsdf', like: 'sdsf' }; - const check3 = {}; - const check4 = { in: '' }; - - const arrayCheck = [check1, check2, check3]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodFilterSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts deleted file mode 100644 index f1c87d68..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { - z, - ZodEffects, - ZodLiteral, - ZodObject, - ZodOptional, - ZodString, - ZodUnion, -} from 'zod'; -import { ZodFilterMap } from './select'; -import { Entity, FilterOperand, ValueOf } from '../../../types'; -import { ObjectTyped } from '../../utils'; -import { - getValidationErrorForStrict, - nonEmptyObject, - oneOf, - stringLongerThan, -} from '../zod-utils'; -import { ConcatRelation, ResultGetField } from '../../orm'; - -export type ZodFilterSchema = ZodEffects< - ZodObject< - { - [key in ValueOf]: ZodOptional< - ZodEffects - >; - }, - 'strict' - > ->; -export const zodFilterSchema: ZodFilterSchema = z - .object( - ObjectTyped.values(FilterOperand).reduce((acum, item) => { - acum[item] = z.string().refine(stringLongerThan()); - return acum; - }, {} as Record>) - ) - .partial() - .strict() - .refine(oneOf(Object.values(FilterOperand)), { - message: `Must have one of: "${Object.values(FilterOperand).join('","')}"`, - }); - -export type ZodFilterFieldSchema = ZodUnion< - [ZodEffects, ZodFilterSchema] ->; -export const zodFilterFieldSchema: ZodFilterFieldSchema = z.union([ - z.string().refine(stringLongerThan()), - zodFilterSchema, -]); - -export type ZodFilterRelationSchema = ZodUnion< - [ - ZodObject<{ - [FilterOperand.eq]: ZodLiteral<'null'>; - }>, - ZodObject<{ - [FilterOperand.ne]: ZodLiteral<'null'>; - }> - ] ->; -export const zodFilterRelationSchema: ZodFilterRelationSchema = z.union([ - z - .object({ - [FilterOperand.eq]: z.literal('null'), - }) - .strict(), - z - .object({ - [FilterOperand.ne]: z.literal('null'), - }) - .strict(), -]); - -export type ZodFilterInputMap = ZodFilterMap< - ResultGetField['field'], - ZodFilterFieldSchema -> & - ZodFilterMap['relations'], ZodFilterRelationSchema> & - ZodFilterMap, ZodFilterFieldSchema>; - -export const getObjectForFilter = < - E extends readonly [string, ...string[]], - R extends boolean ->( - fieldList: E, - isRelation: R -): ZodFilterMap< - E, - R extends true ? ZodFilterRelationSchema : ZodFilterFieldSchema -> => { - return fieldList.reduce( - (acum, item) => ({ - ...acum, - ...{ - [item]: isRelation - ? zodFilterRelationSchema.optional() - : zodFilterFieldSchema.optional(), - }, - }), - {} as ZodFilterMap< - E, - R extends true ? ZodFilterRelationSchema : ZodFilterFieldSchema - > - ); -}; - -export type ZodFilterInputQueryObject = ZodObject< - ZodFilterInputMap, - 'strict' ->; - -export type ZodFilterInputQuerySchema = ZodEffects< - ZodFilterInputQueryObject ->; - -export const zodFilterInputQuerySchema = ( - field: ResultGetField['field'], - relations: ResultGetField['relations'], - relationField: ConcatRelation -): ZodFilterInputQuerySchema => { - const filterMap: ZodFilterInputMap = { - ...getObjectForFilter(field, false), - ...getObjectForFilter(relations, true), - ...getObjectForFilter(relationField, false), - }; - - const zodFilterInputQueryObject: ZodFilterInputQueryObject = z - .object(filterMap) - .strict(getValidationErrorForStrict(Object.keys(filterMap), 'Filter')); - - return zodFilterInputQueryObject.refine(nonEmptyObject(), { - message: 'Validation error: Filter should be not empty', - }); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts deleted file mode 100644 index 6b886e13..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts +++ /dev/null @@ -1,9 +0,0 @@ -// export { -// ZodIncludeQuerySchema as ZodIncludeInputQuerySchema, -// zodIncludeQuerySchema as zodIncludeInputQuerySchema, -// } from '../zod-query-schema/index'; - -import { z, ZodString } from 'zod'; - -export type ZodIncludeInputQuerySchema = ZodString; -export const zodIncludeInputQuerySchema = z.string().min(1); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts deleted file mode 100644 index 4189c7ab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ZodOptional } from 'zod'; - -import { zodFilterInputQuerySchema, ZodFilterInputQuerySchema } from './filter'; -import { - zodSelectFieldsInputQuerySchema, - ZodSelectFieldsInputQuerySchema, -} from './select'; -import { - zodIncludeInputQuerySchema, - ZodIncludeInputQuerySchema, -} from './include'; -import { zodSortInputQuerySchema, ZodSortInputQuerySchema } from './sort'; -import { zodPageInputQuerySchema, ZodPageInputQuerySchema } from './page'; - -import { Entity } from '../../../types'; -import { QueryField } from '../zod-helper'; - -export type ZodInputQueryShape = { - [QueryField.filter]: ZodOptional>; - [QueryField.fields]: ZodOptional>; - [QueryField.include]: ZodOptional; - [QueryField.sort]: ZodOptional; - [QueryField.page]: ZodPageInputQuerySchema; -}; - -export { - zodIncludeInputQuerySchema, - zodSortInputQuerySchema, - zodPageInputQuerySchema, - zodSelectFieldsInputQuerySchema, - zodFilterInputQuerySchema, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts deleted file mode 100644 index 1d3b4900..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - zodPageQuerySchema as zodPageInputQuerySchema, - ZodPageQuerySchema as ZodPageInputQuerySchema, -} from '../zod-query-schema/index'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts deleted file mode 100644 index 2cec1d24..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { zodSelectFieldsInputQuerySchema } from './select'; -import { z, ZodError } from 'zod'; -import { ResultGetField } from '../../orm'; -import { Users } from '../../../mock-utils'; - -describe('Check "zodSelectFieldsInputQuerySchema"', () => { - const arrayForCheck: ResultGetField['relations'] = [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', - ]; - const zodSelectFieldsSchema = - zodSelectFieldsInputQuerySchema(arrayForCheck); - type TypeSelectField = z.infer; - it('Valid schema', () => { - const check1: TypeSelectField = { - target: 'sdfsdfsfd', - notes: 'sdfsdf', - comments: 'sdfsdf', - roles: 'sdfsdf', - }; - const check2: TypeSelectField = { - target: 'sdfsdfsfd', - }; - const check3: TypeSelectField = { - addresses: 'sdfsdf', - manager: 'sdfsdf', - }; - const arrayCheck = [check1, check2, check3]; - for (const item of arrayCheck) { - const result = zodSelectFieldsSchema.parse(item); - expect(result).toEqual(item); - } - }); - it('Invalid schema', () => { - const check1 = {}; - const check2 = { dfd: 'dfsdf', like: 'sdsf' }; - const check3 = null; - const check4 = [] as any[]; - const check5 = 'ssss'; - - const arrayCheck = [check1, check2, check3, check4, check5]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodSelectFieldsSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts deleted file mode 100644 index f2386432..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - z, - ZodEffects, - ZodObject, - ZodOptional, - ZodString, - ZodTypeAny, -} from 'zod'; - -import { ResultGetField } from '../../orm'; -import { Entity } from '../../../types'; -import { getValidationErrorForStrict, nonEmptyObject } from '../zod-utils'; - -export type ZodSelectFieldsInputQuerySchema = ZodEffects< - ZodObject< - ObjectForSelectField['relations'], ZodString>, - 'strict' - > ->; - -export type ZodFilterMap< - R extends readonly [string, ...string[]], - F extends ZodTypeAny -> = { - [K in R[number]]: ZodOptional; -}; - -type ObjectForSelectField< - R extends readonly [string, ...string[]], - K extends ZodTypeAny -> = { - target: ZodOptional; -} & ZodFilterMap; - -const objectForSelectField = ( - relationList: R -): ObjectForSelectField => { - const relation = relationList.reduce( - (acum, item) => ({ ...acum, ...{ [item]: z.string().optional() } }), - {} as ZodFilterMap - ); - - return { - target: z.string().optional(), - ...relation, - }; -}; - -export const zodSelectFieldsInputQuerySchema = ( - relationList: ResultGetField['relations'] -): ZodSelectFieldsInputQuerySchema => { - const resultShape: ObjectForSelectField< - ResultGetField['relations'], - ZodString - > = objectForSelectField['relations']>(relationList); - - return z - .object(resultShape) - .strict(getValidationErrorForStrict(['target', ...relationList], 'Fields')) - .refine(nonEmptyObject(), { - message: 'Validation error: Need select field for target or relation', - }); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts deleted file mode 100644 index 7f44965e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { z, ZodString } from 'zod'; - -export type ZodSortInputQuerySchema = ZodString; -export const zodSortInputQuerySchema: ZodSortInputQuerySchema = z.string(); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts deleted file mode 100644 index 36f36f9a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { ZodFilterQuerySchema, zodFilterQuerySchema } from './filter'; -import { - AllFieldWithTpe, - PropsArray, - RelationTree, - ResultGetField, - TypeField, -} from '../../orm'; -import { - Users, - Roles, - UserGroups, - Comments, - Notes, - Addresses, -} from '../../../mock-utils'; - -describe('Check "filter" zod schema', () => { - const relation: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], - }; - const fields: ResultGetField['field'] = [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ]; - - const usersPropsArray: PropsArray = { - testArrayNull: true, - testReal: true, - }; - const rolesPropsArray: PropsArray = {}; - const userGroupPropsArray: PropsArray = {}; - const commentsPropsArray: PropsArray = {}; - const notesPropsArray: PropsArray = {}; - const addressesPropsArray: PropsArray = { - arrayField: true, - }; - - const propsType: AllFieldWithTpe = { - id: TypeField.number, - login: TypeField.string, - firstName: TypeField.string, - lastName: TypeField.string, - isActive: TypeField.boolean, - createdAt: TypeField.date, - testReal: TypeField.array, - testArrayNull: TypeField.array, - testDate: TypeField.date, - updatedAt: TypeField.date, - addresses: { - id: TypeField.number, - city: TypeField.string, - state: TypeField.string, - country: TypeField.string, - arrayField: TypeField.array, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - manager: { - id: TypeField.number, - login: TypeField.string, - firstName: TypeField.string, - lastName: TypeField.string, - testReal: TypeField.array, - testArrayNull: TypeField.array, - isActive: TypeField.boolean, - createdAt: TypeField.date, - testDate: TypeField.date, - updatedAt: TypeField.date, - }, - roles: { - id: TypeField.number, - name: TypeField.string, - key: TypeField.string, - isDefault: TypeField.boolean, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - comments: { - id: TypeField.number, - text: TypeField.string, - kind: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - notes: { - id: TypeField.string, - text: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - userGroup: { id: TypeField.number, label: TypeField.string }, - }; - - const filterQuerySchema = zodFilterQuerySchema( - fields, - relation, - { - target: usersPropsArray, - roles: rolesPropsArray, - userGroup: userGroupPropsArray, - comments: commentsPropsArray, - notes: notesPropsArray, - manager: usersPropsArray, - addresses: addressesPropsArray, - }, - propsType - ); - type FilterQuerySchema = z.infer>; - - it('Valid schema', () => { - const check1: FilterQuerySchema = { - target: { - id: { - gte: '1213', - ne: '12', - }, - }, - relation: null, - }; - const check2: FilterQuerySchema = { - target: { - id: { - gte: '1213', - }, - login: { - lt: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['sdfsdf', 'sdfsdf'], - }, - }, - }, - }; - const check3: FilterQuerySchema = { - target: null, - relation: null, - }; - const check4: FilterQuerySchema = { - target: null, - relation: { - comments: { - id: { - lte: '123', - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check5: FilterQuerySchema = { - target: null, - relation: { - comments: { - id: { - in: ['1'], - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check6: FilterQuerySchema = { - target: { - id: { - gte: '1213', - ne: '123', - }, - addresses: { - eq: 'null', - }, - }, - relation: null, - }; - const check7: FilterQuerySchema = { - target: { - isActive: { - eq: 'true', - }, - }, - relation: null, - }; - const check8: FilterQuerySchema = { - target: { - createdAt: { - eq: '2023-12-08T09:40:58.020Z', - }, - }, - relation: null, - }; - const check9: FilterQuerySchema = { - target: { - createdAt: { - eq: 'null', - }, - }, - relation: null, - }; - - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - ]; - for (const check of checkArray) { - const result = filterQuerySchema.parse(check); - expect(result).toEqual(check); - } - const result = filterQuerySchema.parse(check9); - expect(result.target!.createdAt!.eq).toEqual(null); - result.target!.createdAt!.eq = 'null'; - expect(result).toEqual(check9); - }); - - it('Invalid schema', () => { - const check1 = null; - const check2 = {}; - const check3 = ''; - const check4 = 1; - const check5: any[] = []; - const check6 = { - target: null, - }; - const check7 = { - target: null, - relation: { - commentsasda: { - id: { - lte: 'sdfsdf', - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check8 = { - target: null, - relation: { - comment: { - id: { - lte: 'sdfsdf', - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - sdfsdf: {}, - }, - }; - const check9 = { - target: { - id: { - gte: '1213', - }, - createdAt: { - lt: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - eq: '1', - }, - }, - }, - }; - const check10 = { - target: { - id: '', - createdAt: { - lt: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['sdfsdf', 'sdfsdf'], - }, - }, - }, - }; - const check11 = { - target: { - createdAt: { - lt1: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['sdfsdf', 'sdfsdf'], - }, - }, - }, - }; - const check12 = { - target: { - createdAt: { - in: 'sdfs', - }, - }, - relation: null, - }; - const check13 = { - target: { - sdfsdf: { - eq: 'sdfs', - }, - }, - relation: null, - }; - const check14 = { - target: { - sdfsdf: { - eq: 'sdfs', - }, - }, - relation: { - addresses: { - sdfsdf: { - eq: 'dsfsdf', - }, - }, - }, - }; - const check15 = { - target: null, - relation: { - addresses: {}, - }, - }; - const check16 = { - target: { - id: { - gte: '1213', - ne: 'sdfsfdsf', - }, - addresses: { - eqa: 'null', - }, - }, - relation: null, - }; - const check17 = { - target: { - id: { - gte: '1213', - ne: 'sdfsfdsf', - }, - addresses: { - eq: 'sdfsdf', - }, - }, - relation: null, - }; - const check18 = { - target: { - id: { - gte: 'invalidType', - ne: 'sdfsfdsf', - }, - addresses: { - eq: 'sdfsdf', - }, - }, - relation: null, - }; - const check19 = { - target: null, - relation: { - comments: { - id: { - in: ['dsf'], - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check20 = { - target: { - isActive: { - eq: 'sdfsdf', - }, - }, - }; - const check21: FilterQuerySchema = { - target: { - createdAt: { - eq: 'sdfasd', - }, - }, - relation: null, - }; - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - check17, - check18, - check19, - check20, - check21, - ]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - filterQuerySchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts deleted file mode 100644 index 99f9c7cb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { - z, - ZodArray, - ZodEffects, - ZodLiteral, - ZodNullable, - ZodObject, - ZodOptional, - ZodString, - ZodUnion, -} from 'zod'; -import { - arrayItemStringLongerThan, - elementOfArrayMustBe, - nonEmptyObject, - oneOf, - stringLongerThan, - stringMustBe, -} from '../zod-utils'; -import { - CastProps, - Entity, - EntityProps, - FilterOperand, - IsArray, - TypeCast, - TypeOfArray, - ValueOf, -} from '../../../types'; -import { ObjectTyped } from '../../utils'; -import { - AllFieldWithTpe, - ArrayPropsForEntity, - guardIsKeyOfObject, - PropsArray, - RelationTree, - ResultGetField, - TypeField, -} from '../../orm'; - -import { - zodFilterRelationSchema, - ZodFilterRelationSchema, -} from '../zod-input-query-schema/filter'; - -type ZodForString = ZodUnion< - [ZodEffects, null, 'null'>, ZodEffects] ->; - -const zodForString: ZodForString = z.union([ - z.literal('null').transform(() => null), - z.string().refine(stringLongerThan(), { - message: 'String should be not empty', - }), -]); - -type ZodForStringArray = ZodEffects< - ZodArray, - [string | null, ...(string | null)[]], - [string, ...string[]] ->; -const zodForStringArray: ZodForStringArray = zodForString - .array() - .nonempty() - .refine(arrayItemStringLongerThan(0), { - message: 'Array should be not empty', - }); - -type OperandForString = Exclude< - ValueOf, - FilterOperand.in | FilterOperand.nin | FilterOperand.some ->; -type OperandForArray = Extract< - ValueOf, - FilterOperand.in | FilterOperand.nin ->; - -type OperandForArrayField = FilterOperand.some; - -type MapOperandForString = { - [K in OperandForString]: ZodOptional; -}; - -type MapOperandForArrayString = { - [K in OperandForArray]: ZodOptional; -}; - -type MapOperand = MapOperandForString & MapOperandForArrayString; -type ZodOperand = ZodEffects>; -type ZodOperandForArrayField = ZodObject< - { - [K in OperandForArrayField]: ZodForStringArray; - }, - 'strict' ->; - -const arrayOperand: { [K in OperandForArray]: boolean } = { - [FilterOperand.nin]: true, - [FilterOperand.in]: true, -}; - -const shapeMapOperand = (type: TypeField = TypeField.string): ZodOperand => - z - .object( - ObjectTyped.entries(FilterOperand) - .filter(([key]) => key !== FilterOperand.some) - .reduce( - (acum, [key, val]) => ({ - ...acum, - [val]: (Object.prototype.hasOwnProperty.call(arrayOperand, key) - ? zodForStringArray.refine(elementOfArrayMustBe(type), { - message: `String should be as ${type}`, - }) - : zodForString.refine(stringMustBe(type), { - message: `String should be as ${type}`, - }) - ).optional(), - }), - {} as MapOperand - ) - ) - .strict() - .refine( - oneOf( - Object.values(FilterOperand).filter((i) => i !== FilterOperand.some) - ), - { - message: `Must have one of: "${Object.values(FilterOperand) - .filter((i) => i !== FilterOperand.some) - .join('","')}"`, - } - ); - -const shapeForArrayField: ZodOperandForArrayField = z - .object({ [FilterOperand.some]: zodForStringArray }) - .strict(); - -type FilterProps = { - [Props in P[number]]: Props extends keyof E - ? IsArray extends true - ? ZodOptional - : ZodOptional - : never; -}; - -type FilterTargetRelation

= { - [Props in P[number]]: ZodOptional; -}; - -const getFilterProps = ( - field: ResultGetField['field'], - propsArray: PropsArray, - propsType: AllFieldWithTpe -): FilterProps['field']> => - field.reduce( - (acum, item) => ({ - ...acum, - [item]: (Reflect.get(propsArray, item) - ? shapeForArrayField - : shapeMapOperand(propsType[item as EntityProps]) - ).optional(), - }), - {} as FilterProps['field']> - ); - -type TargetRelation = FilterTargetRelation< - ResultGetField['relations'] ->; -type TargetProps = FilterProps< - E, - ResultGetField['field'] -> & - TargetRelation; - -type Target = { - target: ZodNullable, 'strict'>>>; -}; - -type RelationType = TypeCast< - TypeOfArray>, - Entity ->; - -type RelationFilter = ZodOptional< - ZodEffects['field']>, 'strict'>> ->; - -type Relation = { - [R in keyof RelationTree]: RelationFilter>; -}; - -type ZodObjectRelation = ZodNullable< - ZodEffects, 'strict'>> ->; - -type ShapeFilter = Target & { - relation: ZodObjectRelation; -}; - -export type ZodFilterQuerySchema = ZodEffects< - ZodObject> ->; - -export const zodFilterQuerySchema = ( - field: ResultGetField['field'], - relationTree: RelationTree, - propsArray: ArrayPropsForEntity, - propsType: AllFieldWithTpe -): ZodFilterQuerySchema => { - const { target: propsArrayTarget, ...relationPropsArray } = propsArray; - - const targetShape: FilterProps['field']> = - getFilterProps(field, propsArrayTarget, propsType); - - const targetRelation = ObjectTyped.keys(relationTree).reduce( - (acum, item) => ({ - ...acum, - [item]: zodFilterRelationSchema.optional(), - }), - {} as TargetRelation - ); - - const targetProps: TargetProps = { - ...targetShape, - ...targetRelation, - }; - - const target: Target = { - target: z.object(targetProps).strict().refine(nonEmptyObject()).nullable(), - }; - - const relationPlaceHolder = ObjectTyped.keys(relationTree).reduce( - (acum, item) => { - acum[item] = undefined; - return acum; - }, - {} as { [K in keyof RelationTree]: undefined } - ); - const relation = ObjectTyped.keys(relationTree).reduce((acum, name) => { - type F = typeof name; - type RT = RelationType; - type RTF = ResultGetField['field']; - - const relationField = relationTree[name] as RTF; - guardIsKeyOfObject(relationPropsArray, name); - - const propsArrayForRelation = relationPropsArray[name] as PropsArray; - - const filterProps = getFilterProps( - relationField, - propsArrayForRelation, - propsType[name] as AllFieldWithTpe - ); - const zodFilter: RelationFilter = z - .object(filterProps) - .strict() - .refine(nonEmptyObject()) - .optional(); - const newName: any = name; - guardIsKeyOfObject(relationPlaceHolder, newName); - - acum[newName] = zodFilter; - return acum; - }, {} as Relation); - - const zodObjectRelation: ZodObjectRelation = z - .object(relation) - .strict() - .refine(nonEmptyObject()) - .nullable(); - - const shapeFilter: ShapeFilter = { - ...target, - ['relation']: zodObjectRelation, - }; - - return z.object(shapeFilter).strict().refine(nonEmptyObject()); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts deleted file mode 100644 index 19f6e82b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { zodIncludeQuerySchema } from './include'; -import { ZodError } from 'zod'; - -describe('Check "include" zod schema', () => { - const relations = [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', - ] as const; - const zodIncludeQuerySchemaResult = zodIncludeQuerySchema(relations); - - it('Valid schema', () => { - const check1 = ['addresses']; - const result1 = zodIncludeQuerySchemaResult.parse(check1); - expect(result1).toEqual(check1); - - const check2 = ['addresses', 'manager']; - const result2 = zodIncludeQuerySchemaResult.parse(check2); - expect(result2).toEqual(check2); - }); - - it('Invalid schema', () => { - const check1: string[] = []; - const check2: string[] = ['test']; - const check3: string[] = ['addresses', 'manager', 'manager']; - - const checkArray = [check1, check2, check3]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - zodIncludeQuerySchemaResult.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts deleted file mode 100644 index eedc60e6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Writeable, z, ZodArray, ZodEffects, ZodEnum } from 'zod'; -import { uniqueArray } from '../zod-utils'; - -export type ZodIncludeQuerySchema = - ZodEffects< - ZodArray>, 'atleastone'>, - [Writeable[number], ...Writeable[number][]], - [Writeable[number], ...Writeable[number][]] - >; - -export const zodIncludeQuerySchema = ( - relationList: U -): ZodIncludeQuerySchema => - z.enum(relationList).array().nonempty().refine(uniqueArray(), { - message: 'Include should have unique relation', - }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts deleted file mode 100644 index 7102e7cb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { zodFilterQuerySchema, ZodFilterQuerySchema } from './filter'; -import { - zodSelectFieldsQuerySchema, - ZodSelectFieldsQuerySchema, -} from './select'; -import { zodIncludeQuerySchema, ZodIncludeQuerySchema } from './include'; -import { zodSortQuerySchema, ZodSortQuerySchema } from './sort'; -import { zodPageQuerySchema, ZodPageQuerySchema } from './page'; -import { QueryField } from '../zod-helper'; -import { Entity } from '../../../types'; -import { ResultGetField } from '../../orm'; -import { ZodNullable } from 'zod'; - -export { - zodPageQuerySchema, - ZodPageQuerySchema, - zodIncludeQuerySchema, - ZodIncludeQuerySchema, - zodFilterQuerySchema, - ZodFilterQuerySchema, - zodSelectFieldsQuerySchema, - ZodSelectFieldsQuerySchema, - zodSortQuerySchema, - ZodSortQuerySchema, -}; - -export type ZodQueryShape = { - [QueryField.filter]: ZodFilterQuerySchema; - [QueryField.fields]: ZodNullable>; - [QueryField.include]: ZodNullable< - ZodIncludeQuerySchema['relations']> - >; - [QueryField.sort]: ZodNullable>; - [QueryField.page]: ZodPageQuerySchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts deleted file mode 100644 index 36a69cba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { zodPageQuerySchema } from './page'; -import { ZodError } from 'zod'; -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../constants'; - -describe('Check "page" zod schema', () => { - it('Valid schema', () => { - const defaultPage = { size: DEFAULT_PAGE_SIZE, number: DEFAULT_QUERY_PAGE }; - const check1 = { size: 1, number: 1 }; - const check2 = { size: 1 }; - const check3 = undefined; - const check4 = { number: 1 }; - const check5 = {}; - const check6 = { size: '1', number: '1' }; - const result1 = zodPageQuerySchema.parse(check1); - const result2 = zodPageQuerySchema.parse(check2); - const result3 = zodPageQuerySchema.parse(check3); - const result4 = zodPageQuerySchema.parse(check4); - const result5 = zodPageQuerySchema.parse(check5); - const result6 = zodPageQuerySchema.parse(check6); - expect(result1).toEqual(check1); - expect(result2).toEqual({ - ...defaultPage, - ...check2, - }); - expect(result3).toEqual(defaultPage); - expect(result4).toEqual({ - ...defaultPage, - ...check4, - }); - expect(result5).toEqual(defaultPage); - expect(result6).toEqual(check1); - }); - - it('Invalid schema', () => { - const check1 = { size: 0 }; - const check2 = { size: -1 }; - const check3 = { size: 'sdfsdf' }; - const check4 = { size: -1, number: '21ad' }; - const check5 = { size: -1, number: -1 }; - const check6 = { size: 'sdsad', number: '21ad' }; - const check7 = { size: 1, number: 2, otherProps: 'dsfsdf' }; - const check8 = { size: 1, otherProps: 'dsfsdf' }; - - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - ]; - - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - zodPageQuerySchema.parse(check1); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts deleted file mode 100644 index 3a35c0bf..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { z, ZodDefault, ZodEffects, ZodNumber, ZodObject } from 'zod'; -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../constants'; - -export type ZodPageQuerySchema = ZodDefault< - ZodObject< - { - size: ZodEffects, number, unknown>; - number: ZodEffects, number, unknown>; - }, - 'strict' - > ->; - -const checkNumber = (a: unknown) => { - if (typeof a === 'string') { - return parseInt(a, 10); - } else if (typeof a === 'number') { - return a; - } else { - return undefined; - } -}; -export const zodPageQuerySchema: ZodPageQuerySchema = z - .object({ - size: z.preprocess( - checkNumber, - z.number().int().min(1).default(DEFAULT_PAGE_SIZE) - ), - number: z.preprocess( - checkNumber, - z.number().int().min(1).default(DEFAULT_QUERY_PAGE) - ), - }) - .strict() - .default({ - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts deleted file mode 100644 index e493afe3..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { RelationTree, ResultGetField } from '../../orm'; -import { Users } from '../../../mock-utils'; -import { - ZodSelectFieldsQuerySchema, - zodSelectFieldsQuerySchema, -} from './select'; -import { z, ZodError } from 'zod'; - -describe('Check "select" zod schema', () => { - const relation: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], - }; - const fields: ResultGetField['field'] = [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ]; - - const selectQuerySchema = zodSelectFieldsQuerySchema(fields, relation); - type SelectTypeQuery = z.infer>; - - it('Valid schema', () => { - const check1: SelectTypeQuery = { - target: ['id', 'createdAt', 'isActive'], - userGroup: ['label'], - roles: ['createdAt'], - }; - const check2: SelectTypeQuery = { - userGroup: ['label'], - roles: ['createdAt'], - }; - const check3: SelectTypeQuery = { - addresses: ['city'], - }; - const checkArray = [check1, check2, check3]; - for (const check of checkArray) { - const result = selectQuerySchema.parse(check); - expect(result).toEqual(check); - } - }); - - it('Invalid schema', () => { - const check1 = {}; - const check2 = null; - const check3 = ''; - const check4: [] = []; - const check5 = { - target: ['id', 'createdAt', 'isActive'], - userGroup1: ['label'], - roles: ['createdAt', 'createdAt'], - }; - const check6 = { - target1: ['id', 'createdAt', 'isActive'], - userGroup: ['label'], - roles: ['createdAt', 'createdAt'], - }; - const check7 = { - target: ['id1', 'createdAt', 'isActive'], - userGroup: ['label'], - }; - const check8 = { - target: ['id1', 'createdAt', 'isActive'], - userGroup: ['label'], - roles: ['createdAt1', 'createdAt'], - }; - const check9 = { - target: '', - userGroup: ['label'], - }; - const check10 = { - target: {}, - userGroup: ['label'], - }; - const check11 = { - target: [], - userGroup: ['label'], - }; - const check12 = { - target: '', - userGroup: ['label'], - }; - const check13 = { - target: null, - userGroup: ['label'], - }; - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - ]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - selectQuerySchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts deleted file mode 100644 index ca3690be..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - Writeable, - z, - ZodArray, - ZodEffects, - ZodEnum, - ZodObject, - ZodOptional, -} from 'zod'; - -import { Entity } from '../../../types'; -import { RelationTree, ResultGetField } from '../../orm'; -import { ObjectTyped } from '../../utils'; -import { nonEmptyObject, uniqueArray } from '../zod-utils'; - -type ZodField = ZodEffects< - ZodArray>, 'atleastone'>, - [Writeable[number], ...Writeable[number][]], - [Writeable[number], ...Writeable[number][]] ->; - -const zodFields = ( - fields: R -): ZodField => - z.enum(fields).array().nonempty().refine(uniqueArray(), { - message: 'Field should be unique', - }); - -type ZodTarget = { - target: ZodOptional>; -}; - -export const zodTarget = ( - fields: R -): ZodTarget => ({ - target: zodFields([...fields]).optional(), -}); - -type ZodRelation = { - [K in keyof RelationTree]: ZodOptional[K]>>; -}; - -type ZodResultShape = ZodTarget['field']> & - ZodRelation; - -export type ZodSelectFields = ZodEffects< - ZodObject, 'strict'> ->; - -export type ZodSelectFieldsQuerySchema = ZodEffects< - ZodSelectFields ->; -export const zodSelectFieldsQuerySchema = ( - fields: ResultGetField['field'], - relationList: RelationTree -): ZodSelectFieldsQuerySchema => { - const target: ZodTarget['field']> = zodTarget(fields); - - const relation = ObjectTyped.entries(relationList).reduce( - (acum, [key, val]) => { - acum[key] = zodFields(val).optional(); - return acum; - }, - {} as ZodRelation - ); - - const resultShape: ZodResultShape = { - ...target, - ...relation, - }; - const zodSelectFields = z - .object(resultShape) - .strict('Should be only target of relation '); - // @ts-ignore - return zodSelectFields.refine(nonEmptyObject(), { - message: 'Validation error: Select target or relation fields', - }) as unknown as ZodSelectFieldsQuerySchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts deleted file mode 100644 index cfb82568..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { zodSortQuerySchema, ZodSortQuerySchema } from './sort'; -import { RelationTree, ResultGetField } from '../../orm'; -import { Users } from '../../../mock-utils'; - -describe('Check "sort" zod schema', () => { - const relation: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], - }; - const fields: ResultGetField['field'] = [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ]; - - const sortQuerySchema = zodSortQuerySchema(fields, relation); - type SortTypeQuery = z.infer>; - it('Valid schema', () => { - const check1: SortTypeQuery = { - target: { - id: 'DESC', - createdAt: 'ASC', - }, - }; - const check2: SortTypeQuery = { - comments: { - kind: 'ASC', - createdAt: 'ASC', - }, - roles: { - id: 'ASC', - }, - }; - const checkArray = [check1, check2]; - for (const check of checkArray) { - const result = sortQuerySchema.parse(check); - expect(result).toEqual(check); - } - }); - it('Invalid schema', () => { - const check1 = ''; - const check2 = null; - const check3: string[] = ['test']; - const check4: string[] = ['addresses', 'manager', 'manager']; - const check5 = {}; - const check6 = { - sdfsf: null, - }; - const check7 = { - sdfsf: null, - sfdsdf: null, - }; - - const check8 = { - comments: {}, - }; - const check9 = { - comments: null, - }; - const check10 = { - comments: 's', - }; - const check11 = { - comments: [], - }; - const check12 = { - comments: { - kindsdfsdf: 'ASC', - }, - }; - const check13 = { - comments: { - kind: '', - }, - }; - const check14 = { - comments: { - kind: 'sdfsdf', - }, - }; - const check15 = { - comments: { - kind: [], - }, - }; - const check16 = { - comments: { - kind: {}, - }, - }; - const check17 = { - target: {}, - }; - const check18 = { - target: null, - }; - const check19 = { - target: 's', - }; - const check20 = { - target: [], - }; - const check21 = { - target: { - idsdfsdf: 'ASC', - }, - }; - const check22 = { - target: { - id: '', - }, - }; - const check23 = { - target: { - id: 'sdfsdf', - }, - }; - const check24 = { - target: { - id: [], - }, - }; - const check25 = { - target: { - id: {}, - }, - }; - - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - check17, - check18, - check19, - check20, - check21, - check22, - check23, - check24, - check25, - ]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - sortQuerySchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts deleted file mode 100644 index a2d89c06..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Entity } from '../../../types'; -import { z, ZodEffects, ZodEnum, ZodObject, ZodOptional } from 'zod'; -import { RelationTree, ResultGetField } from '../../orm'; -import { SORT_TYPE } from '../../../constants'; -import { nonEmptyObject } from '../zod-utils'; - -import { ObjectTyped } from '../../utils'; - -export type SortTarget = { - target: SortEntityOptionalObject; -}; - -export const sortTarget = ( - fields: R -): SortTarget => ({ - target: z.object(sortEntity(fields)).strict().refine(nonEmptyObject()), -}); - -export const sortEntity = ( - fields: R -): SortEntityOptional => - fields.reduce( - (acum, item) => ({ - ...acum, - ...{ - [item]: z.enum(SORT_TYPE).optional(), - }, - }), - {} as SortEntityOptional - ); - -export type SortEnum = ZodEnum<['DESC', 'ASC']>; - -export type SortEntityOptional = { - [K in F[number]]: ZodOptional; -}; - -export type SortEntityOptionalObject = - ZodEffects, 'strict'>>; - -export type SortRelation = { - [k in keyof RelationTree]: SortEntityOptionalObject[k]>; -}; - -export type Sort = SortTarget['field']> & - SortRelation; - -export type OptionalSort = { - [k in keyof Sort]: ZodOptional[k]>; -}; - -type ZodBaseObjectSchema = ZodObject, 'strict'>; -type OptionalZodBaseObjectSchema = ZodObject< - OptionalSort, - 'strict' ->; -export type ZodSortQuerySchema = ZodEffects< - OptionalZodBaseObjectSchema ->; - -export const zodSortQuerySchema = ( - fields: ResultGetField['field'], - relation: RelationTree -): ZodSortQuerySchema => { - const sortTargetObject = sortTarget(fields); - - const sortRelationObject = ObjectTyped.entries(relation).reduce( - (acum, [item, fields]) => { - return { - ...acum, - ...{ - [item]: z - .object(sortEntity(fields)) - .strict() - .refine(nonEmptyObject()), - }, - }; - }, - {} as SortRelation - ); - - const sortMerge: Sort = { - ...sortTargetObject, - ...sortRelationObject, - }; - - const baseZodSchema: ZodBaseObjectSchema = z.object(sortMerge).strict(); - - const partialZod = baseZodSchema.partial() as OptionalZodBaseObjectSchema; - - return partialZod.refine( - nonEmptyObject() - ) as unknown as ZodSortQuerySchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts deleted file mode 100644 index a9bc2bb6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - oneOf, - stringLongerThan, - arrayItemStringLongerThan, - uniqueArray, - nonEmptyObject, -} from './zod-utils'; - -describe('zod-utils', () => { - let checkFunk: (arg: any) => boolean; - describe('oneOf', () => { - beforeAll(() => { - checkFunk = oneOf(['props', 'props1', 'props2']); - }); - - it('Should be ok', () => { - const result1 = checkFunk({ props: 1 }); - const result2 = checkFunk({ props: 1, other: 1 }); - const result3 = checkFunk({ props2: 1, other: 1 }); - expect(result1).toBe(true); - expect(result2).toBe(true); - expect(result3).toBe(true); - }); - - it('Should be not ok', () => { - const result1 = checkFunk({}); - const result2 = checkFunk({ other: 1 }); - const result3 = checkFunk({ props4: 1, other: 1 }); - expect(result1).toBe(false); - expect(result2).toBe(false); - expect(result3).toBe(false); - }); - }); - - describe('stringLongerThan', () => { - beforeAll(() => { - checkFunk = stringLongerThan(5); - }); - it('Should be ok', () => { - const result = checkFunk('123456'); - expect(result).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk('12345'); - const result2 = checkFunk('1234'); - expect(result).toBe(false); - expect(result2).toBe(false); - }); - }); - - describe('arrayItemStringLongerThan', () => { - beforeAll(() => { - checkFunk = arrayItemStringLongerThan(5); - }); - it('Should be ok', () => { - const result = checkFunk(['123456', '123456']); - expect(result).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk(['12345', '123456']); - const result2 = checkFunk(['1234', '123456']); - const result3 = checkFunk(['123456', '1234']); - expect(result).toBe(false); - expect(result2).toBe(false); - expect(result3).toBe(false); - }); - }); - - describe('uniqueArray', () => { - beforeAll(() => { - checkFunk = uniqueArray(); - }); - it('Should be ok', () => { - const result = checkFunk(['1', '2', '3', '4']); - const result2 = checkFunk([1, 2, 3, 4]); - expect(result).toBe(true); - expect(result2).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk(['1', '1', '123456']); - const result2 = checkFunk(['1', '123456', '1']); - const result3 = checkFunk([1, '1234', 1]); - expect(result).toBe(false); - expect(result2).toBe(false); - expect(result3).toBe(false); - }); - }); - - describe('nonEmptyObject', () => { - beforeAll(() => { - checkFunk = nonEmptyObject(); - }); - it('Should be ok', () => { - const result = checkFunk({ test: 1 }); - expect(result).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk({}); - const result2 = checkFunk(undefined); - expect(result).toBe(false); - expect(result2).toBe(false); - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts deleted file mode 100644 index f265e25b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { TypeField } from '../orm'; - -export const oneOf = (keys: string[]) => (val: any) => { - for (const k of keys) { - if (val[k] !== undefined) return true; - } - return false; -}; - -export const stringLongerThan = - (length = 0) => - (str: string) => - str.length > length; - -export const arrayItemStringLongerThan = - (length = 0) => - (array: [string | null, ...(string | null)[]]) => { - const checkFunction = stringLongerThan(length); - return !array.some((i) => i !== null && !checkFunction(i)); - }; - -export const stringMustBe = - (type: TypeField = TypeField.string) => - (inputString: string | null) => { - if (inputString === null) return true; - switch (type) { - case TypeField.boolean: - return inputString === 'true' || inputString === 'false'; - case TypeField.number: - return !isNaN(+inputString); - case TypeField.date: - return new Date(inputString).toString() !== 'Invalid Date'; - default: - return true; - } - }; -export const elementOfArrayMustBe = - (type: TypeField = TypeField.string) => - (inputArray: unknown[]) => { - const checkFunc = stringMustBe(type); - return !inputArray.some((i) => !checkFunc(`${i}`)); - }; -export const uniqueArray = () => (e: string[]) => new Set(e).size === e.length; - -export const nonEmptyObject = - () => - (object: T) => - !!(object && Object.keys(object).length > 0); - -export const getValidationErrorForStrict = ( - props: string[], - name: 'Fields' | 'Filter' -) => - `Validation error: ${name} should be have only props: ["${props.join( - '","' - )}"]`; diff --git a/libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts b/libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts deleted file mode 100644 index 641fb5c3..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { DynamicModule, Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { ModuleOptions } from './types'; -import { GLOBAL_MODULE_OPTIONS_TOKEN } from './constants'; -import { CurrentDataSourceProvider, SwaggerBindMethod } from './factory'; -import { TransformInputService, EntityPropsMapService } from './service'; - -@Module({}) -export class JsonApiNestJsCommonModule { - static forRoot(options: ModuleOptions): DynamicModule { - const optionProvider = { - provide: GLOBAL_MODULE_OPTIONS_TOKEN, - useValue: options, - }; - - const currentDataSourceProvider = CurrentDataSourceProvider( - options.connectionName - ); - - const typeOrmModule = TypeOrmModule.forFeature( - options.entities, - options.connectionName - ); - - return { - module: JsonApiNestJsCommonModule, - imports: [typeOrmModule, ...(options.imports || [])], - providers: [ - ...(options.providers || []), - optionProvider, - currentDataSourceProvider, - TransformInputService, - EntityPropsMapService, - SwaggerBindMethod, - ], - exports: [ - ...(options.providers || []), - typeOrmModule, - optionProvider, - currentDataSourceProvider, - TransformInputService, - EntityPropsMapService, - SwaggerBindMethod, - ...(options.imports || []), - ], - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts deleted file mode 100644 index 9f54a008..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { DynamicModule, Module } from '@nestjs/common'; -import { DiscoveryModule, RouterModule } from '@nestjs/core'; -import { ConfigParamDefault, DEFAULT_CONNECTION_NAME } from './constants'; -import { ModuleOptions } from './types'; -import { JsonApiNestJsCommonModule } from './json-api-nestjs-common.module'; -import { JSON_API_DECORATOR_ENTITY } from './constants'; -import { BaseModuleClass } from './mixin'; -import { AtomicOperationModule } from './modules/atomic-operation/atomic-operation.module'; - -@Module({}) -export class JsonApiModule { - private static connectionName = DEFAULT_CONNECTION_NAME; - - public static forRoot(options: ModuleOptions): DynamicModule { - JsonApiModule.connectionName = - options.connectionName || JsonApiModule.connectionName; - - options.connectionName = JsonApiModule.connectionName; - options.options = { - ...ConfigParamDefault, - ...options.options, - }; - - const commonModule = JsonApiNestJsCommonModule.forRoot(options); - - const entityImport = options.entities.map((entity) => { - const controller = (options.controllers || []).find( - (item) => - item && - Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, item) === entity - ); - const module = BaseModuleClass.forRoot({ - entity, - connectionName: JsonApiModule.connectionName, - controller, - config: { - ...ConfigParamDefault, - ...options.options, - }, - }); - module.imports = [ - DiscoveryModule, - commonModule, - ...(module.imports || []), - ]; - return module; - }); - - const operationModuleImport = options.options?.operationUrl - ? [ - AtomicOperationModule.forRoot( - { - ...options, - connectionName: JsonApiModule.connectionName, - }, - entityImport, - commonModule - ), - RouterModule.register([ - { - module: AtomicOperationModule, - path: options.options.operationUrl, - }, - ]), - ] - : []; - - return { - module: JsonApiModule, - imports: [...operationModuleImport, ...entityImport], - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts deleted file mode 100644 index 0581ba74..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - MethodName, - Entity, - TypeormService, - EntityRelation, - ResourceObject, - ResourceObjectRelationships, -} from '../../types'; -import { - PostData, - Query, - PatchData, - PostRelationshipData, - PatchRelationshipData, -} from '../../helper'; -import { TYPEORM_SERVICE_PROPS } from '../../constants'; - -type RequestMethodeObject = { [k in MethodName]: (...arg: any[]) => any }; - -interface IJsonBaseController extends RequestMethodeObject {} - -export class JsonBaseController - implements IJsonBaseController -{ - private [TYPEORM_SERVICE_PROPS]!: TypeormService; - - getOne(id: string | number, query: Query): Promise> { - return this[TYPEORM_SERVICE_PROPS].getOne(id, query); - } - getAll(query: Query): Promise> { - return this[TYPEORM_SERVICE_PROPS].getAll(query); - } - deleteOne(id: string | number): Promise { - return this[TYPEORM_SERVICE_PROPS].deleteOne(id); - } - - patchOne( - id: string | number, - inputData: PatchData - ): Promise> { - return this[TYPEORM_SERVICE_PROPS].patchOne(id, inputData); - } - - postOne(inputData: PostData): Promise> { - return this[TYPEORM_SERVICE_PROPS].postOne(inputData); - } - - getRelationship>( - id: string | number, - relName: Rel - ): Promise> { - return this[TYPEORM_SERVICE_PROPS].getRelationship(id, relName); - } - postRelationship>( - id: string | number, - relName: Rel, - input: PostRelationshipData - ): Promise> { - return this[TYPEORM_SERVICE_PROPS].postRelationship(id, relName, input); - } - - deleteRelationship>( - id: string | number, - relName: Rel, - input: PostRelationshipData - ): Promise { - return this[TYPEORM_SERVICE_PROPS].deleteRelationship(id, relName, input); - } - - patchRelationship>( - id: string | number, - relName: Rel, - input: PatchRelationshipData - ): Promise> { - return this[TYPEORM_SERVICE_PROPS].patchRelationship(id, relName, input); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/index.ts deleted file mode 100644 index b71f2177..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './module/module.mixin'; -export * from './controller/json-base.controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts deleted file mode 100644 index 53f35403..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - InternalServerErrorException, - CallHandler, - ExecutionContext, - Inject, - Injectable, - NestInterceptor, -} from '@nestjs/common'; -import { catchError, Observable, throwError } from 'rxjs'; -import { QueryFailedError, Repository } from 'typeorm'; -import { DriverUtils } from 'typeorm/driver/DriverUtils'; - -import { - CONTROL_OPTIONS_TOKEN, - CURRENT_ENTITY_REPOSITORY, -} from '../../constants'; -import { ConfigParam, Entity, ValidateQueryError } from '../../types'; -import { - MysqlError, - MysqlErrorCode, - PostgresError, - PostgresErrorCode, -} from '../../helper'; -import { HttpException } from '@nestjs/common'; - -@Injectable() -export class ErrorInterceptors implements NestInterceptor { - @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; - @Inject(CONTROL_OPTIONS_TOKEN) private config!: ConfigParam; - - intercept( - context: ExecutionContext, - next: CallHandler - ): Observable | Promise> { - return next.handle().pipe( - catchError((error) => { - if (error instanceof QueryFailedError) { - return throwError(() => this.prepareDataBaseError(error)); - } - - if (error instanceof HttpException) { - return throwError(() => error); - } - - const errorObject: ValidateQueryError = { - code: 'internal_error', - message: this.config.debug ? error.message : 'Internal Server Error', - path: [], - }; - const descriptionOrOptions = this.config.debug ? error : undefined; - return throwError( - () => - new InternalServerErrorException( - [errorObject], - descriptionOrOptions - ) - ); - }) - ); - } - - private prepareDataBaseError(error: QueryFailedError): HttpException { - const errorObject: ValidateQueryError = { - code: 'internal_error', - message: this.config.debug ? error.message : 'Internal Server Error', - path: [], - }; - - if (DriverUtils.isMySQLFamily(this.repository.manager.connection.driver)) { - const { errorCode, errorMsg } = this.prepareMysqlError(error.driverError); - if (MysqlError[errorCode]) { - return MysqlError[errorCode](this.repository.metadata, errorMsg); - } - } - - if ( - DriverUtils.isPostgresFamily(this.repository.manager.connection.driver) - ) { - const { errorCode, errorMsg, detail } = this.preparePostgresError( - error.driverError - ); - - if (PostgresError[errorCode]) { - return PostgresError[errorCode]( - this.repository.metadata, - errorMsg, - detail - ); - } - } - - return new InternalServerErrorException([errorObject]); - } - - private prepareMysqlError(error: any): { - errorCode: MysqlErrorCode; - errorMsg: string; - } { - return { - errorCode: error.errno, - errorMsg: error.message, - }; - } - - private preparePostgresError(error: any): { - errorCode: PostgresErrorCode; - errorMsg: string; - detail: string; - } { - return { - errorCode: error.code, - errorMsg: error.message, - detail: error.detail, - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts deleted file mode 100644 index b6030081..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './error.interceptors'; -export * from './log-time.interceptors'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts deleted file mode 100644 index badabaed..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; -import { map, Observable } from 'rxjs'; - -import { Entity, ResourceObject } from '../../types'; - -export class LogTimeInterceptors implements NestInterceptor { - intercept( - context: ExecutionContext, - next: CallHandler> - ): Observable | Promise> { - const now = Date.now(); - return next.handle().pipe( - map((r) => { - const response = context.switchToHttp().getResponse(); - const time = Date.now() - now; - response.setHeader('x-response-time', time); - if (r && r.meta) { - r.meta['time'] = time; - } - - return r; - }) - ); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts deleted file mode 100644 index 04225e97..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { DynamicModule } from '@nestjs/common'; - -import { BaseModuleOptions, ConfigParam, DecoratorOptions } from '../../types'; -import { - createController, - getProviderName, - nameIt, - bindController, -} from '../../helper'; -import { - JSON_API_DECORATOR_OPTIONS, - ConfigParamDefault, - JSON_API_MODULE_POSTFIX, - CONTROL_OPTIONS_TOKEN, -} from '../../constants'; -import { - ZodInputQuerySchema, - ZodQuerySchema, - TypeormServiceFactory, - EntityRepositoryFactory, - ZodInputPostSchema, - ZodInputPatchSchema, - ZodInputPostRelationshipSchema, - ZodInputPatchRelationshipSchema, -} from '../../factory'; -import { TypeormUtilsService } from '../service'; -import { TransformDataService } from '../service'; -import { SwaggerBindService } from '../service/swagger-bind.service'; - -export class BaseModuleClass { - static forRoot(options: BaseModuleOptions): DynamicModule { - const { entity, connectionName, controller } = options; - const controllerClass = createController(entity, controller); - - const decoratorOptions: DecoratorOptions = - Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, controllerClass) || {}; - - const moduleConfig: ConfigParam = { - ...ConfigParamDefault, - ...options.config, - ...decoratorOptions, - }; - - bindController(controllerClass, entity, connectionName, moduleConfig); - - const optionProvider = { - provide: CONTROL_OPTIONS_TOKEN, - useValue: moduleConfig, - }; - - return { - module: nameIt( - getProviderName(entity, JSON_API_MODULE_POSTFIX), - BaseModuleClass - ), - controllers: [controllerClass], - providers: [ - ZodInputQuerySchema(entity), - ZodQuerySchema(entity), - ZodInputPostSchema(entity), - ZodInputPatchSchema(entity), - ZodInputPostRelationshipSchema, - ZodInputPatchRelationshipSchema, - TypeormServiceFactory(entity), - EntityRepositoryFactory(entity), - optionProvider, - TypeormUtilsService, - TransformDataService, - SwaggerBindService, - ], - imports: [], - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts deleted file mode 100644 index 2e84898d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - pullUser, - Users, -} from '../../../mock-utils'; -import { CheckItemEntityPipe } from './check-item-entity.pipe'; -import { TypeormUtilsService } from '../../../service'; - -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../../factory'; -import { - CURRENT_ENTITY_REPOSITORY, - DEFAULT_CONNECTION_NAME, -} from '../../../constants'; -import { Repository } from 'typeorm'; -import { NotFoundException } from '@nestjs/common'; - -describe('CheckItemEntityPipe', () => { - let db: IMemoryDb; - let checkItemEntityPipe: CheckItemEntityPipe; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(Users), - TypeormUtilsService, - CheckItemEntityPipe, - ], - }).compile(); - - await pullUser(module.get>(CURRENT_ENTITY_REPOSITORY)); - - checkItemEntityPipe = - module.get>(CheckItemEntityPipe); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be correct', async () => { - const id = 1; - const result = await checkItemEntityPipe.transform(id); - expect(result).toBe(id); - }); - - it('Should be error', async () => { - expect.assertions(1); - try { - await checkItemEntityPipe.transform(11111); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts deleted file mode 100644 index a42cb10d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Inject, NotFoundException, PipeTransform } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { CURRENT_ENTITY_REPOSITORY } from '../../../constants'; -import { Entity, ValidateQueryError } from '../../../types'; -import { TypeormUtilsService } from '../../service/typeorm-utils.service'; - -export class CheckItemEntityPipe - implements PipeTransform> -{ - @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; - @Inject(TypeormUtilsService) - private typeormUtilsService!: TypeormUtilsService; - async transform(value: I): Promise { - const params = 'params'; - const result = await this.repository - .createQueryBuilder(this.typeormUtilsService.currentAlias) - .where( - `${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - )} = :${params}` - ) - .setParameters({ - [params]: value, - }) - .getOne(); - if (!result) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${value}' does not exist`, - path: [], - }; - throw new NotFoundException([error]); - } - return value; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts deleted file mode 100644 index 256cdb57..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './check-item-entity.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts deleted file mode 100644 index fe926be1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable, ParseIntPipe } from '@nestjs/common'; - -import { QueryPipe } from './query'; -import { QueryInputPipe } from './query-input'; -import { QueryFiledInIncludePipe } from './query-filed-on-include'; -import { QueryCheckSelectField } from './query-check-select-field'; - -import { ConfigParam, Entity, PipeMixin } from '../../types'; -import { nameIt } from '../../helper'; -import { CheckItemEntityPipe } from './check-item-entity'; -import { PostInputPipe } from './post-input'; -import { PatchInputPipe } from './patch-input'; -import { PostRelationshipPipe } from './post-relationship'; -import { ParseRelationshipNamePipe } from './parse-relationship-name'; -import { PatchRelationshipPipe } from './patch-relationship'; - -function factoryMixin(entity: Entity, connectionName: string, pipe: PipeMixin) { - const entityName = - entity instanceof Function ? entity.name : entity['options'].name; - - const pipeClass = nameIt( - `${entityName.charAt(0).toUpperCase() + entityName.slice(1)}${pipe.name}`, - pipe - ) as PipeMixin; - - Injectable()(pipeClass); - - return pipeClass; -} - -export function queryInputMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, QueryInputPipe); -} - -export function queryMixin(entity: Entity, connectionName: string): PipeMixin { - return factoryMixin(entity, connectionName, QueryPipe); -} - -export function queryFiledInIncludeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, QueryFiledInIncludePipe); -} - -export function queryCheckSelectFieldMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, QueryCheckSelectField); -} - -export function idPipeMixin( - entity: Entity, - connectionName: string, - config?: ConfigParam -): PipeMixin { - return config && config.pipeForId ? config.pipeForId : ParseIntPipe; -} - -export function checkItemEntityPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, CheckItemEntityPipe); -} -export function postInputPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PostInputPipe); -} - -export function patchInputPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PatchInputPipe); -} - -export function postRelationshipPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PostRelationshipPipe); -} - -export function patchRelationshipPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PatchRelationshipPipe); -} -export function parseRelationshipNamePipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, ParseRelationshipNamePipe); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts deleted file mode 100644 index 62c4518d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './parse-relationship-name.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts deleted file mode 100644 index 11fbf094..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; -import { ParseRelationshipNamePipe } from './parse-relationship-name.pipe'; -import { TypeormUtilsService } from '../../service/typeorm-utils.service'; - -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../constants'; -import { UnprocessableEntityException } from '@nestjs/common'; - -describe('ParseRelationshipNamePipe', () => { - let db: IMemoryDb; - let parseRelationshipNamePipe: ParseRelationshipNamePipe; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(Users), - TypeormUtilsService, - ParseRelationshipNamePipe, - ], - }).compile(); - - parseRelationshipNamePipe = module.get>( - ParseRelationshipNamePipe - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be correct', async () => { - const relName = 'userGroup'; - const result = await parseRelationshipNamePipe.transform(relName); - expect(result).toBe(relName); - }); - - it('Should be error', async () => { - expect.assertions(1); - try { - await parseRelationshipNamePipe.transform('11111'); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts deleted file mode 100644 index 3963f03e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - PipeTransform, - UnprocessableEntityException, - Inject, -} from '@nestjs/common'; -import { TypeormUtilsService } from '../../service/typeorm-utils.service'; -import { Entity, EntityRelation, ValidateQueryError } from '../../../types'; - -export class ParseRelationshipNamePipe - implements PipeTransform> -{ - @Inject(TypeormUtilsService) - private typeormUtilsService!: TypeormUtilsService; - - transform(value: string): EntityRelation { - this.checkRelName(value); - return value; - } - - checkRelName(value: unknown): asserts value is EntityRelation { - if (!this.typeormUtilsService.relationFields.find((i) => i === value)) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Relation '${value}' does not exist in resource '${this.typeormUtilsService.currentAlias}'`, - path: [], - }; - throw new UnprocessableEntityException([error]); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts deleted file mode 100644 index 6d849c7a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './patch-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts deleted file mode 100644 index 817ad6bb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { DEFAULT_CONNECTION_NAME, ZOD_PATCH_SCHEMA } from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { PatchInputPipe } from './patch-input.pipe'; -import { ZodInputPostSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PatchInputPipe', () => { - let db: IMemoryDb; - let patchInputPipe: PatchInputPipe; - let zodInputPostSchema: ZodInputPostSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_PATCH_SCHEMA, - useValue: { - parse() {}, - }, - }, - PatchInputPipe, - ], - }).compile(); - - patchInputPipe = module.get>(PatchInputPipe); - zodInputPostSchema = - module.get>(ZOD_PATCH_SCHEMA); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPostSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(patchInputPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts deleted file mode 100644 index ec524211..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; - -import { Entity, JSONValue } from '../../../types'; -import { PatchData, ZodInputPatchSchema } from '../../../helper/zod'; -import { ZOD_PATCH_SCHEMA } from '../../../constants'; - -export class PatchInputPipe - implements PipeTransform> -{ - @Inject(ZOD_PATCH_SCHEMA) - private zodInputPatchSchema!: ZodInputPatchSchema; - transform(value: JSONValue): PatchData { - try { - return this.zodInputPatchSchema.parse(value, { - errorMap: errorMap, - })['data'] as PatchData; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts deleted file mode 100644 index 99ef16e8..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './patch-relationship.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts deleted file mode 100644 index b5c13c6f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - ZOD_PATCH_RELATIONSHIP_SCHEMA, -} from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, -} from '../../../mock-utils'; - -import { PatchRelationshipPipe } from './patch-relationship.pipe'; -import { ZodInputPostRelationshipSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PatchInputPipe', () => { - let db: IMemoryDb; - let patchRelationshipPipe: PatchRelationshipPipe; - let zodInputPatchRelationshipSchema: ZodInputPostRelationshipSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, - useValue: { - parse() {}, - }, - }, - PatchRelationshipPipe, - ], - }).compile(); - - patchRelationshipPipe = module.get( - PatchRelationshipPipe - ); - zodInputPatchRelationshipSchema = - module.get(ZOD_PATCH_RELATIONSHIP_SCHEMA); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPatchRelationshipSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(patchRelationshipPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest - .spyOn(zodInputPatchRelationshipSchema, 'parse') - .mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - patchRelationshipPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest - .spyOn(zodInputPatchRelationshipSchema, 'parse') - .mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - patchRelationshipPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts deleted file mode 100644 index e200d2f5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; - -import { JSONValue } from '../../../types'; -import { - PatchRelationshipData, - ZodInputPatchRelationshipSchema, -} from '../../../helper/zod'; -import { ZOD_PATCH_RELATIONSHIP_SCHEMA } from '../../../constants'; - -export class PatchRelationshipPipe - implements PipeTransform -{ - @Inject(ZOD_PATCH_RELATIONSHIP_SCHEMA) - private zodInputPatchRelationshipSchema!: ZodInputPatchRelationshipSchema; - transform(value: JSONValue): PatchRelationshipData { - try { - return this.zodInputPatchRelationshipSchema.parse(value, { - errorMap: errorMap, - })['data']; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts deleted file mode 100644 index 58efc255..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './post-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts deleted file mode 100644 index c61f5be1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { DEFAULT_CONNECTION_NAME, ZOD_POST_SCHEMA } from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { PostInputPipe } from './post-input.pipe'; -import { ZodInputPostSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PostInputPipe', () => { - let db: IMemoryDb; - let postInputPipe: PostInputPipe; - let zodInputPostSchema: ZodInputPostSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_POST_SCHEMA, - useValue: { - parse() {}, - }, - }, - PostInputPipe, - ], - }).compile(); - - postInputPipe = module.get>(PostInputPipe); - zodInputPostSchema = module.get>(ZOD_POST_SCHEMA); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPostSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(postInputPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - postInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - postInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts deleted file mode 100644 index 2eac0b78..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; - -import { Entity, JSONValue } from '../../../types'; -import { PostData, ZodInputPostSchema } from '../../../helper/zod'; -import { ZOD_POST_SCHEMA } from '../../../constants'; - -export class PostInputPipe - implements PipeTransform> -{ - @Inject(ZOD_POST_SCHEMA) private zodInputPostSchema!: ZodInputPostSchema; - transform(value: JSONValue): PostData { - try { - return this.zodInputPostSchema.parse(value, { - errorMap: errorMap, - })['data'] as PostData; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts deleted file mode 100644 index 70457a78..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './post-relationship.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts deleted file mode 100644 index aa1b0c9a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - ZOD_POST_RELATIONSHIP_SCHEMA, -} from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, -} from '../../../mock-utils'; - -import { PostRelationshipPipe } from './post-relationship.pipe'; -import { ZodInputPostRelationshipSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PostInputPipe', () => { - let db: IMemoryDb; - let postRelationshipPipe: PostRelationshipPipe; - let zodInputPostRelationshipSchema: ZodInputPostRelationshipSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_POST_RELATIONSHIP_SCHEMA, - useValue: { - parse() {}, - }, - }, - PostRelationshipPipe, - ], - }).compile(); - - postRelationshipPipe = - module.get(PostRelationshipPipe); - zodInputPostRelationshipSchema = module.get( - ZOD_POST_RELATIONSHIP_SCHEMA - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPostRelationshipSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(postRelationshipPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest - .spyOn(zodInputPostRelationshipSchema, 'parse') - .mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - postRelationshipPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest - .spyOn(zodInputPostRelationshipSchema, 'parse') - .mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - postRelationshipPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts deleted file mode 100644 index 73f5a5e4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; - -import { JSONValue } from '../../../types'; -import { - PostRelationshipData, - ZodInputPostRelationshipSchema, -} from '../../../helper/zod'; -import { ZOD_POST_RELATIONSHIP_SCHEMA } from '../../../constants'; - -export class PostRelationshipPipe - implements PipeTransform -{ - @Inject(ZOD_POST_RELATIONSHIP_SCHEMA) - private zodInputPostRelationshipSchema!: ZodInputPostRelationshipSchema; - transform(value: JSONValue): PostRelationshipData { - try { - return this.zodInputPostRelationshipSchema.parse(value, { - errorMap: errorMap, - })['data']; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts deleted file mode 100644 index 7ba4ed4e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './query-check-select-field'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts deleted file mode 100644 index f2c089d9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { QueryCheckSelectField } from './query-check-select-field'; -import { Users } from '../../../mock-utils'; -import { CONTROL_OPTIONS_TOKEN } from '../../../constants'; -import { Query, QueryField } from '../../../helper/zod'; -import { ConfigParam, Entity } from '../../../types'; -import { BadRequestException } from '@nestjs/common'; - -function getDefaultQuery() { - const filter = { - relation: null, - target: null, - }; - const defaultQuery: Query = { - [QueryField.filter]: filter, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: 1, - number: 1, - }, - }; - - return defaultQuery; -} - -describe('QueryCheckSelectField', () => { - let queryCheckSelectField: QueryCheckSelectField; - let configParam: ConfigParam; - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - QueryCheckSelectField, - ], - }).compile(); - - queryCheckSelectField = module.get>( - QueryCheckSelectField - ); - configParam = module.get(CONTROL_OPTIONS_TOKEN); - }); - - it('Is valid', () => { - const query = getDefaultQuery(); - expect(queryCheckSelectField.transform(query)).toEqual(query); - }); - - it('Is invalid', () => { - const query = getDefaultQuery(); - jest.mocked(configParam).requiredSelectField = true; - expect.assertions(1); - try { - queryCheckSelectField.transform(query); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts deleted file mode 100644 index 9e8579c2..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BadRequestException, Inject, PipeTransform } from '@nestjs/common'; -import { CONTROL_OPTIONS_TOKEN } from '../../../constants'; -import { ConfigParam, Entity, ValidateQueryError } from '../../../types'; -import { Query } from '../../../helper'; - -export class QueryCheckSelectField - implements PipeTransform, Query> -{ - @Inject(CONTROL_OPTIONS_TOKEN) private configParam!: ConfigParam; - transform(value: Query): Query { - if (this.configParam.requiredSelectField && value.fields === null) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Fields params in query is required'`, - path: ['fields'], - }; - throw new BadRequestException([error]); - } - return value; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts deleted file mode 100644 index 82b1b95f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './query-filed-in-include.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts deleted file mode 100644 index c056f542..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; -import { QueryFiledInIncludePipe } from './query-filed-in-include.pipe'; -import { Users } from '../../../mock-utils'; -import { Query, QueryField } from '../../../helper'; - -describe('QueryFiledInIncludePipe', () => { - let queryFiledInIncludePipe: QueryFiledInIncludePipe; - - beforeAll(() => { - queryFiledInIncludePipe = new QueryFiledInIncludePipe(); - }); - - it('Should be ok', () => { - const check: Query = { - [QueryField.fields]: { - roles: ['id'], - }, - [QueryField.include]: ['roles'], - [QueryField.filter]: { - target: null, - relation: null, - }, - [QueryField.sort]: null, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - - const check2: Query = { - [QueryField.fields]: null, - [QueryField.include]: ['roles'], - [QueryField.filter]: { - target: null, - relation: { - roles: { name: { eq: 'test' } }, - }, - }, - [QueryField.sort]: null, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - - const result = queryFiledInIncludePipe.transform(check); - expect(result).toEqual(check); - const result2 = queryFiledInIncludePipe.transform(check2); - expect(result2).toEqual(check2); - }); - - it('Should be not ok', () => { - const check: Query = { - [QueryField.fields]: { - roles: ['id'], - }, - [QueryField.include]: null, - [QueryField.filter]: { - target: null, - relation: null, - }, - [QueryField.sort]: null, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - const check2: Query = { - [QueryField.fields]: { - roles: ['id'], - }, - [QueryField.include]: null, - [QueryField.filter]: { - target: null, - relation: null, - }, - [QueryField.sort]: { - addresses: { - id: 'ASC', - }, - }, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - - const check3: Query = { - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.filter]: { - target: null, - relation: { - roles: { name: { eq: 'test' } }, - }, - }, - [QueryField.sort]: null, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - expect.assertions(3); - try { - queryFiledInIncludePipe.transform(check); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - try { - queryFiledInIncludePipe.transform(check2); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - try { - queryFiledInIncludePipe.transform(check3); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts deleted file mode 100644 index 076cf0c7..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { BadRequestException, PipeTransform } from '@nestjs/common'; -import { Entity, ValidateQueryError } from '../../../types'; -import { ObjectTyped, Query } from '../../../helper'; -export class QueryFiledInIncludePipe - implements PipeTransform, Query> -{ - transform(value: Query): Query { - const errors: ValidateQueryError[] = []; - - const { fields, include, sort, filter } = value; - const includeSet = new Set(); - - if (include) { - include.reduce((acum, item) => acum.add(item), includeSet); - } - - if (filter) { - const { relation } = filter; - if (relation) { - const filterRelationFields = ObjectTyped.keys(relation); - const filterFieldsErrors = filterRelationFields - .filter((i) => !includeSet.has(i.toString())) - .map((i) => ({ - code: 'invalid_intersection_types', - message: `Add '${i.toString()}' to query param 'include'`, - path: ['filter', 'relation', i.toString()], - })); - - errors.push(...filterFieldsErrors); - } - } - - if (fields) { - const { target: targetResourceFields, ...relationFields } = fields; - const selectRelationFields = ObjectTyped.keys(relationFields); - const fieldsErrors = selectRelationFields - .filter((i) => !includeSet.has(i.toString())) - .map((i) => ({ - code: 'invalid_intersection_types', - message: `Add '${i.toString()}' to query param 'include'`, - path: ['fields'], - })); - - errors.push(...fieldsErrors); - } - - if (sort) { - const { target: targetResourceSorts, ...relationSorts } = sort; - const selectRelationFields = ObjectTyped.keys(relationSorts); - const fieldsErrors = selectRelationFields - .filter((i) => !includeSet.has(i.toString())) - .map((i) => ({ - code: 'invalid_intersection_types', - message: `Add '${i.toString()}' to query param 'include'`, - path: ['sort'], - })); - - errors.push(...fieldsErrors); - } - - if (errors.length > 0) { - throw new BadRequestException(errors); - } - - return value; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts deleted file mode 100644 index 7422bf5a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './query-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts deleted file mode 100644 index 115d88ee..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { IMemoryDb } from 'pg-mem'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { - CurrentDataSourceProvider, - ZodInputQuerySchema, -} from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - DEFAULT_PAGE_SIZE, - DEFAULT_QUERY_PAGE, - ZOD_INPUT_QUERY_SCHEMA, -} from '../../../constants'; -import { QueryInputPipe } from './query-input.pipe'; -import { - QueryField, - ZodInputQuerySchema as TypeZodInputQuerySchema, -} from '../../../helper'; - -const page = { - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, -}; - -describe('QueryInputPipe', () => { - let db: IMemoryDb; - let queryInputPipe: QueryInputPipe; - let zodParse: TypeZodInputQuerySchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - ZodInputQuerySchema(Users), - QueryInputPipe, - ], - }).compile(); - - queryInputPipe = module.get>(QueryInputPipe); - zodParse = module.get>( - ZOD_INPUT_QUERY_SCHEMA - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', () => { - const input = {}; - const result = queryInputPipe.transform(input); - - const input1 = { - [QueryField.page]: page[QueryField.page], - }; - const result1 = queryInputPipe.transform(input1); - - const input2 = { - ...page, - [QueryField.fields]: { target: 'test', addresses: '' }, - }; - const result2 = queryInputPipe.transform(input2); - - expect(result).toEqual(page); - expect(result1).toEqual(input1); - expect(result2).toEqual(input2); - }); - - it('Should be not ok', () => { - const input = { - [QueryField.page]: { - size: 0, - number: 'sdfsdf', - }, - sdaas: 'sdfsdf', - [QueryField.include]: null, - [QueryField.fields]: { - dsada: 'sdfsf', - sdfsdfsdfs: 'sdfsdf', - }, - [QueryField.sort]: null, - [QueryField.filter]: { - id: { - eq: 'null', - }, - }, - }; - expect.assertions(1); - try { - const result = queryInputPipe.transform(input); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodParse, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - const result = queryInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts deleted file mode 100644 index 9d740098..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - Inject, - PipeTransform, - BadRequestException, - InternalServerErrorException, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; -import { ZOD_INPUT_QUERY_SCHEMA } from '../../../constants'; -import { ZodInputQuerySchema, InputQuery } from '../../../helper'; -import { Entity, JSONValue } from '../../../types'; - -export class QueryInputPipe - implements PipeTransform> -{ - @Inject(ZOD_INPUT_QUERY_SCHEMA) - private zodInputQuerySchema!: ZodInputQuerySchema; - - transform(value: JSONValue): InputQuery { - try { - return this.zodInputQuerySchema.parse(value, { - errorMap: errorMap, - }); - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts deleted file mode 100644 index a8853ae0..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './query.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts deleted file mode 100644 index e874744a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; -import { QueryPipe } from '../query'; -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; -import { - QueryField, - ZodQuerySchema as TypeZodQuerySchema, -} from '../../../helper'; - -import { CurrentDataSourceProvider, ZodQuerySchema } from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - ZOD_QUERY_SCHEMA, - DEFAULT_PAGE_SIZE, - DEFAULT_QUERY_PAGE, -} from '../../../constants'; -import { TransformInputService } from '../../../service'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; - -describe('QueryPipe', () => { - let db: IMemoryDb; - let queryPipe: QueryPipe; - let zodParse: TypeZodQuerySchema; - let transformInputService: TransformInputService; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - ZodQuerySchema(Users), - TransformInputService, - QueryPipe, - ], - }).compile(); - - queryPipe = module.get>(QueryPipe); - zodParse = module.get>(ZOD_QUERY_SCHEMA); - transformInputService = module.get>( - TransformInputService - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', () => { - const filter = { - target: null, - relation: null, - }; - - const result = queryPipe.transform({ - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }); - expect(result).toEqual({ - [QueryField.filter]: filter, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }); - }); - - it('Should be not ok', () => { - const input = { - filter: { - test: '', - }, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }; - expect.assertions(1); - try { - const result = queryPipe.transform(input as any); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest - .spyOn(transformInputService, 'transformSort') - .mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - const result = queryPipe.transform({} as any); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts deleted file mode 100644 index 1ea63146..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; - -import { InputQuery, Query, QueryField, ZodQuerySchema } from '../../../helper'; -import { Entity, JSONValue } from '../../../types'; -import { ZOD_QUERY_SCHEMA } from '../../../constants'; -import { TransformInputService } from '../../../service'; - -export class QueryPipe - implements PipeTransform, Query> -{ - @Inject(ZOD_QUERY_SCHEMA) - private zodQuerySchema!: ZodQuerySchema; - - @Inject(TransformInputService) - private transformInputService!: TransformInputService; - - transform(value: InputQuery): Query { - try { - const { filter, page, sort, include, fields } = value; - const queryObject: JSONValue = { - [QueryField.filter]: this.transformInputService.transformFilter(filter), - [QueryField.fields]: this.transformInputService.transformFields(fields), - [QueryField.include]: - this.transformInputService.transformInclude(include), - [QueryField.sort]: this.transformInputService.transformSort(sort), - [QueryField.page]: page, - }; - return this.zodQuerySchema.parse(queryObject); - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts deleted file mode 100644 index 442fee03..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './typeorm-utils.service'; -export * from './transform-data.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts deleted file mode 100644 index 6ffaf430..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import { DiscoveryService } from '@nestjs/core'; -import { PARAMTYPES_METADATA } from '@nestjs/common/constants'; -import { ApiExtraModels, ApiTags } from '@nestjs/swagger'; -import { DECORATORS } from '@nestjs/swagger/dist/constants'; -import { JsonBaseController } from '../controller/json-base.controller'; -import { Repository } from 'typeorm'; -import { - CONTROL_OPTIONS_TOKEN, - CURRENT_ENTITY_REPOSITORY, - JSON_API_CONTROLLER_POSTFIX, - JSON_API_DECORATOR_ENTITY, - SWAGGER_METHOD, -} from '../../constants'; - -import { - createApiModels, - ObjectTyped, - swaggerMethod, - SwaggerMethod, - FilterOperand, - nameIt, - getProviderName, -} from '../../helper'; -import { Bindings } from '../../config/bindings'; -import { DecoratorOptions, Entity } from '../../types'; - -@Injectable() -export class SwaggerBindService implements OnModuleInit { - @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; - @Inject(DiscoveryService) private discoveryService!: DiscoveryService; - @Inject(SWAGGER_METHOD) private swaggerMethod!: SwaggerMethod; - @Inject(CONTROL_OPTIONS_TOKEN) private config!: DecoratorOptions; - - public initSwagger() { - const controllerName = nameIt( - getProviderName(this.repository.target, JSON_API_CONTROLLER_POSTFIX), - JsonBaseController - ).name; - - const controllerInst = this.discoveryService - .getControllers() - .find( - (i) => - i.name === controllerName || - this.repository.target === - Reflect.getMetadata( - JSON_API_DECORATOR_ENTITY, - i.instance.constructor - ) - ); - if (!controllerInst) - throw new Error( - `Controller for ${this.repository.metadata.name} is empty` - ); - const controller = controllerInst.instance.constructor; - const apiTag = Reflect.getMetadata(DECORATORS.API_TAGS, controller); - if (!apiTag) { - ApiTags(this.config['overrideRoute'] || this.repository.metadata.name)( - controller - ); - } - - ApiTags(this.repository.metadata.name)(controller); - - ApiExtraModels(FilterOperand)(controller); - ApiExtraModels(createApiModels(this.repository))(controller); - - const { allowMethod = Object.keys(Bindings) } = this.config; - for (const method of ObjectTyped.keys(Bindings)) { - if (!allowMethod.includes(method)) continue; - - if (method in swaggerMethod) { - swaggerMethod[method]( - controller.prototype, - this.repository, - Bindings[method], - this.config - ); - } - Reflect.defineMetadata( - PARAMTYPES_METADATA, - [Object], - controller.prototype, - method - ); - } - } - - onModuleInit(): any { - this.initSwagger(); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts deleted file mode 100644 index 777d29a5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../constants'; -import { TransformDataService } from './transform-data.service'; -import { TypeormUtilsService } from './typeorm-utils.service'; -import { ApplicationConfig } from '@nestjs/core'; -import { VersioningType } from '@nestjs/common'; -import { EntityPropsMapService } from '../../service'; -import { data } from '../../helper/zod/zod-input-post-relationship-schema'; - -describe('TransformDataService', () => { - let db: IMemoryDb; - let transformDataService: TransformDataService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - let applicationConfig: ApplicationConfig; - let entityPropsMapService: EntityPropsMapService; - let usersData: Users; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - TransformDataService, - EntityPropsMapService, - ], - }).compile(); - - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - - // transformDataService = - // module.get>(TransformDataService); - entityPropsMapService = module.get( - EntityPropsMapService - ); - - const data = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - userGroup: true, - manager: true, - roles: true, - addresses: true, - comments: true, - notes: true, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - usersData = data; - applicationConfig = module.get(ApplicationConfig); - }); - - beforeEach(() => { - transformDataService = new TransformDataService(); - Object.defineProperty(transformDataService, 'applicationConfig', { - value: applicationConfig, - }); - Object.defineProperty(transformDataService, 'entityPropsMapService', { - value: entityPropsMapService, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Url path', () => { - const prefix = 'api'; - applicationConfig.setGlobalPrefix(prefix); - - expect(transformDataService.urlPath).toEqual(['', prefix]); - }); - it('Url path with version', () => { - const prefix = 'api'; - const version = '1'; - - applicationConfig.setGlobalPrefix(prefix); - - jest - .spyOn(applicationConfig, 'getVersioning') - .mockImplementationOnce(() => ({ - type: VersioningType.URI, - defaultVersion: version, - })); - - expect(transformDataService.urlPath).toEqual(['', prefix, 'v1']); - }); - - it('check type, id and links', () => { - const result = transformDataService.transformData(usersData); - expect(result).toHaveProperty('data'); - const { data } = result; - expect(data.type).toBe('users'); - expect(data.id).toBe(usersData.id.toString()); - expect(data.links.self).toBe( - [...transformDataService.urlPath, 'users', usersData.id.toString()].join( - '/' - ) - ); - }); - - it('check attributes', async () => { - const result = transformDataService.transformData(usersData); - const { - id, - addresses, - comments, - manager, - roles, - notes, - userGroup, - ...other - } = usersData; - expect(result.data.attributes).toEqual(other); - const result1 = transformDataService.transformData([]); - expect(result1.data).toEqual([]); - - const data = await userRepository.findOne({ - select: { - id: true, - login: true, - isActive: true, - testDate: true, - }, - where: { - id: 1, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - const result2 = transformDataService.transformData(data); - const { id: id2, ...other2 } = data; - - expect(result2.data.attributes).toEqual(other2); - }); - - it('check relationships', async () => { - const { - data: { relationships }, - } = transformDataService.transformData(usersData); - expect(relationships?.addresses?.data).toEqual({ - type: 'addresses', - id: usersData.addresses.id.toString(), - }); - expect(relationships?.addresses?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/addresses` - ); - expect(relationships?.manager?.data).toEqual({ - type: 'users', - id: usersData.manager.id.toString(), - }); - expect(relationships?.manager?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/manager` - ); - expect(relationships?.roles?.data).toEqual( - usersData.roles.map((i) => ({ - type: 'roles', - id: i.id.toString(), - })) - ); - expect(relationships?.roles?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/roles` - ); - expect(relationships?.comments?.data).toEqual( - usersData.comments.map((i) => ({ - type: 'comments', - id: i.id.toString(), - })) - ); - expect(relationships?.comments?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/comments` - ); - expect(relationships?.notes?.data).toEqual( - usersData.notes.map((i) => ({ - type: 'notes', - id: i.id.toString(), - })) - ); - expect(relationships?.notes?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/notes` - ); - expect(relationships?.userGroup?.data).toEqual({ - type: 'user-groups', - id: usersData.userGroup.id.toString(), - }); - expect(relationships?.userGroup?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/userGroup` - ); - }); - - it('check relationships again', async () => { - const data = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - userGroup: true, - roles: true, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - - const { - data: { relationships }, - } = transformDataService.transformData(data); - - expect(relationships?.addresses).not.toHaveProperty('data'); - expect(relationships?.addresses?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/addresses` - ); - expect(relationships?.manager).not.toHaveProperty('data'); - expect(relationships?.manager?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/manager` - ); - expect(relationships?.roles?.data).toEqual( - usersData.roles.map((i) => ({ - type: 'roles', - id: i.id.toString(), - })) - ); - expect(relationships?.roles?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/roles` - ); - expect(relationships?.comments).not.toHaveProperty('data'); - expect(relationships?.comments?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/comments` - ); - expect(relationships?.notes).not.toHaveProperty('data'); - expect(relationships?.notes?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/notes` - ); - expect(relationships?.userGroup?.data).toEqual({ - type: 'user-groups', - id: usersData.userGroup.id.toString(), - }); - expect(relationships?.userGroup?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/userGroup` - ); - data.userGroup = null as any; - data.roles = []; - const { - data: { relationships: relationships2 }, - } = transformDataService.transformData(data); - - expect(relationships2?.addresses).not.toHaveProperty('data'); - expect(relationships2?.addresses?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/addresses` - ); - expect(relationships2?.manager).not.toHaveProperty('data'); - expect(relationships2?.manager?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/manager` - ); - - expect(relationships2?.roles?.data).toEqual([]); - expect(relationships2?.roles?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/roles` - ); - expect(relationships2?.comments).not.toHaveProperty('data'); - expect(relationships2?.comments?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/comments` - ); - expect(relationships2?.notes).not.toHaveProperty('data'); - expect(relationships2?.notes?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/notes` - ); - expect(relationships2?.userGroup?.data).toEqual(null); - expect(relationships2?.userGroup?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/userGroup` - ); - }); - - it('check include', async () => { - const data = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - userGroup: true, - roles: true, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - - const { included } = transformDataService.transformData(data); - expect(included).not.toBe(undefined); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts deleted file mode 100644 index 7fac3b28..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { Inject, Injectable, VersioningType } from '@nestjs/common'; -import { ApplicationConfig } from '@nestjs/core'; - -import { - Attributes, - Data, - Entity, - EntityRelation, - MainData, - Relationships, - ResourceData, -} from '../../types'; -import { ResourceObject } from '../../types/response'; -import { camelToKebab, ObjectTyped } from '../../helper'; -import { RoutePathFactory } from '@nestjs/core/router/route-path-factory'; -import { EntityPropsMapService } from '../../service'; - -Injectable(); -export class TransformDataService { - @Inject(ApplicationConfig) private applicationConfig!: ApplicationConfig; - @Inject(EntityPropsMapService) - private entityPropsMapService!: EntityPropsMapService; - - private _urlPath!: string[]; - - get urlPath() { - if (this._urlPath) return [...this._urlPath]; - this._urlPath = ['']; - const prefix = this.applicationConfig.getGlobalPrefix(); - const version = this.applicationConfig.getVersioning(); - - const routePathFactory = new RoutePathFactory(this.applicationConfig); - - if (prefix) { - this._urlPath.push(this.applicationConfig.getGlobalPrefix()); - } - if (version && version.type === VersioningType.URI) { - const firstVersion = Array.isArray(version.defaultVersion) - ? version.defaultVersion[0] - : version.defaultVersion; - if (firstVersion) { - this._urlPath.push( - `${routePathFactory.getVersionPrefix( - version - )}${firstVersion.toString()}` - ); - } - } - return [...this._urlPath]; - } - private getLink(...partOfUrl: string[]) { - const urlPath = this.urlPath; - urlPath.push(...partOfUrl); - return urlPath.join('/'); - } - - public transformData(data: E): Pick, 'data' | 'included'>; - public transformData( - data: E[] - ): Pick, 'data' | 'included'>; - public transformData( - data: E | E[] - ): - | Pick, 'data' | 'included'> - | Pick, 'data' | 'included'> { - const dataResponse = Array.isArray(data) - ? data - .map((i) => this.getMainData(i)) - .filter((i: ResourceData | null): i is ResourceData => !!i) - : this.getMainData(data); - - let relationships: Relationships | undefined = undefined; - if (Array.isArray(dataResponse) && dataResponse.length > 0) { - relationships = dataResponse.reduce( - (acum, item) => ({ - ...acum, - ...item.relationships, - }), - {} as Relationships - ); - } - - if (!Array.isArray(dataResponse) && dataResponse !== null) { - relationships = dataResponse.relationships; - } - - if (relationships === undefined) { - return { data: dataResponse } as Pick< - ResourceObject, - 'data' | 'included' - >; - } - - const propsNeedForInclude = ObjectTyped.entries(relationships).reduce( - (acum, [key, val]) => { - if (!val || !('data' in val)) { - return acum; - } - if (Array.isArray(val.data)) { - acum.push(key); - return acum; - } - if (!Array.isArray(val.data)) { - acum.push(key); - } - return acum; - }, - [] as EntityRelation[] - ); - - const included = (Array.isArray(data) ? data : [data]) - .reduce((acum, item) => { - const tmp = propsNeedForInclude.reduce((a, props) => { - const currentItem = item[props]; - type CurrentItem = typeof currentItem; - const r = ( - Array.isArray(currentItem) ? currentItem : [currentItem] - ) as CurrentItem[]; - - a.push(...r); - return a; - }, [] as E[EntityRelation][]); - acum.push(...tmp); - return acum; - }, [] as E[EntityRelation][]) - .map((i) => { - return this.getMainData(i as E); - }) - .filter((i) => !!i); - - if (included.length > 0) { - return { data: dataResponse, included } as unknown as Pick< - ResourceObject, - 'data' | 'included' - >; - } - return { data: dataResponse } as unknown as Pick< - ResourceObject, - 'data' | 'included' - >; - } - - getMainData(data: E): ResourceData | null { - if (!data) return null; - const entity = data.constructor as E; - return { - ...this.getData( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[this.entityPropsMapService.getPrimaryColumnsForEntity(entity)] as - | string - | number - ), - attributes: ObjectTyped.entries(data).reduce((acum, [key, val]) => { - if ( - this.entityPropsMapService - .getRelPropsForEntity(entity) - .includes(key.toString()) || - key.toString() === - this.entityPropsMapService.getPrimaryColumnsForEntity(entity) - ) { - return acum; - } - acum[key] = val; - return acum; - }, {} as Record) as Attributes, - relationships: this.transformRelationshipsData(data), - links: { - self: this.getLink( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[ - this.entityPropsMapService.getPrimaryColumnsForEntity(entity) - ] as string - ), - }, - }; - } - - getRelationships>( - data: E, - rel: Rel - ): Data { - const entity = data.constructor as E; - const relation = data[rel]; - const target = this.entityPropsMapService.getRelationPropsType(entity, rel); - const typeName = camelToKebab( - this.entityPropsMapService.getNameForEntity(target) - ) as Rel; - - const primaryColumn = - this.entityPropsMapService.getPrimaryColumnsForEntity(target); - - const resultData = Array.isArray(relation) - ? (relation as E[Rel][]).map((i: E[Rel]) => - this.getData( - typeName, - i[primaryColumn as keyof E[Rel]] as string - ) - ) - : relation === null || !relation - ? null - : this.getData( - typeName, - relation[primaryColumn as keyof E[Rel]] as string - ); - - return { - data: resultData, - } as Data; - } - - transformRelationshipsData(data: E): Relationships { - const entity = data.constructor as E; - return this.entityPropsMapService - .getRelPropsForEntity(entity) - .reduce((acum, val) => { - const result: { - links: Record; - data?: Data], EntityRelation>['data']; - } = { - links: { - self: this.getLink( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[ - this.entityPropsMapService.getPrimaryColumnsForEntity( - entity - ) as keyof E - ] as string, - 'relationships', - val - ), - }, - }; - - if (val in data) { - const { data: dataRelationships } = this.getRelationships( - data, - val as EntityRelation - ); - if (Array.isArray(dataRelationships)) { - result['data'] = dataRelationships; - } - - if (!Array.isArray(dataRelationships)) { - result['data'] = dataRelationships; - } - } - acum[val.toString()] = result; - return acum; - }, {} as Record) as Relationships; - } - - getData(type: T, id: number | string): MainData { - return { - type, - id: id.toString(), - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts deleted file mode 100644 index 3682c037..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts +++ /dev/null @@ -1,768 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - UserGroups, - Users, - Comments, - Roles, - Addresses, - Notes, - getRepository, - pullAllData, -} from '../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../factory'; -import { - CURRENT_ENTITY_REPOSITORY, - DEFAULT_CONNECTION_NAME, -} from '../../constants'; -import { TypeormUtilsService } from './typeorm-utils.service'; -import { - PostData, - PostRelationshipData, - Query, - QueryField, -} from '../../helper'; -import { - EXPRESSION, - FilterOperand, - OperandsMapExpression, - Entity, -} from '../../types'; -import { - BadRequestException, - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; - -function getDefaultQuery() { - const filter = { - relation: null, - target: null, - }; - const defaultQuery: Query = { - [QueryField.filter]: filter, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: 1, - number: 1, - }, - }; - - return defaultQuery; -} - -describe('TypeormUtilsService', () => { - let db: IMemoryDb; - let typeormUtilsServiceUserGroups: TypeormUtilsService; - let repositoryUserGroups: Repository; - - let typeormUtilsServiceUser: TypeormUtilsService; - let repositoryUser: Repository; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - function getQuery() { - return repositoryUser - .createQueryBuilder() - .subQuery() - .select('Users-Roles.user_id') - .from('users_have_roles', 'Users-Roles') - .leftJoin( - Roles, - 'Users__Roles_roles', - 'Users-Roles.role_id = Users__Roles_roles.id' - ); - } - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(UserGroups), - TypeormUtilsService, - ], - }).compile(); - - typeormUtilsServiceUserGroups = - module.get>(TypeormUtilsService); - repositoryUserGroups = module.get>( - CURRENT_ENTITY_REPOSITORY - ); - - const moduleUsers: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(Users), - TypeormUtilsService, - ], - }).compile(); - - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - - typeormUtilsServiceUser = - moduleUsers.get>(TypeormUtilsService); - repositoryUser = moduleUsers.get>( - CURRENT_ENTITY_REPOSITORY - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('TypeormUtilsService.currentAlias', () => { - expect(typeormUtilsServiceUserGroups.currentAlias).toBe('UserGroups'); - }); - - it('TypeormUtilsService.getAliasForRelation', () => { - expect(typeormUtilsServiceUserGroups.getAliasForRelation('users')).toBe( - 'UserGroups__Users_users' - ); - }); - - it('TypeormUtilsService.getAliasPath', () => { - expect(typeormUtilsServiceUserGroups.getAliasPath('id')).toBe( - 'UserGroups.id' - ); - expect( - typeormUtilsServiceUserGroups.getAliasPath('Users', 'UserGroups') - ).toBe('UserGroups.Users'); - expect( - typeormUtilsServiceUserGroups.getAliasPath('Users', 'UserGroups', '-') - ).toBe('UserGroups-Users'); - expect( - typeormUtilsServiceUserGroups.getAliasPath('label', 'users', '-') - ).toBe('Users-label'); - }); - - describe('asyncIterateFindRelationships', () => { - it('should be ok', async () => { - const notes = await notesRepository.find(); - const userGroup = await userGroupRepository.find(); - - const data: PostData['relationships'] = { - notes: [ - { - type: 'notes', - id: notes[0].id, - }, - ], - manager: { - type: 'users', - id: '1', - }, - userGroup: { - type: 'users-group', - id: `${userGroup[0].id}`, - }, - }; - - const result = []; - for await (const item of typeormUtilsServiceUser.asyncIterateFindRelationships( - data - )) { - result.push(item); - } - - expect(result[0]).toHaveProperty('notes'); - expect(result[0]['notes']).toEqual([{ id: notes[0].id }]); - - expect(result[1]).toHaveProperty('manager'); - expect(result[1]['manager']).toEqual({ id: 1 }); - - expect(result[2]).toHaveProperty('userGroup'); - expect(result[2]['userGroup']).toEqual({ id: userGroup[0].id }); - }); - - it('should be error props incorrect', async () => { - const data = { - incorrectProps: { - type: 'users', - id: '1', - }, - } as any; - expect.assertions(1); - try { - await typeormUtilsServiceUser - .asyncIterateFindRelationships(data) - .next(); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('should be error resource not found', async () => { - const data: PostData['relationships'] = { - manager: { - id: '1000', - type: 'users', - }, - }; - expect.assertions(1); - try { - await typeormUtilsServiceUser - .asyncIterateFindRelationships(data) - .next(); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - }); - - describe('getFilterExpressionForTarget', () => { - it('expression for target field with null', () => { - - const nullableField = 'id' - const notNullableField = 'login' - const query = getDefaultQuery(); - query.filter.target = { - [nullableField]: { - [FilterOperand.eq]: null, - }, - [notNullableField]: { - [FilterOperand.ne]: null, - }, - }; - - function guardField( - filter: any, - field: any - ): asserts field is keyof R { - if (filter && !(field in filter)) - throw new Error('field not exist in query filter'); - } - - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - const mainAliasCheck = 'Users'; - - - for (const item of result) { - const { params, alias, expression, selectInclude } = item; - expect(selectInclude).toBe(undefined); - if (!alias) { - expect(alias).not.toBe(undefined); - throw new Error('alias in undefined for result'); - } - const [mainAlias, field] = alias.split('.'); - expect(mainAlias).toBe(mainAliasCheck); - guardField(query.filter.target, field); - const filterName: any = query.filter.target[field]; - if (!filterName) { - expect(filterName).not.toBe(undefined); - throw new Error('filterName in undefined from query'); - } - - expect(params).toBe(undefined) - - if (field === nullableField) { - expect(expression).toBe('IS NULL'); - continue; - } - - if (field === notNullableField) { - expect(expression).toBe('IS NOT NULL'); - continue; - } - - throw new Error('filed is incorrect'); - } - }) - it('expression for target field', () => { - const valueTest = (filterOperand: FilterOperand) => - `test for ${filterOperand}`; - const valueTestArray = ( - filterOperand: FilterOperand.nin | FilterOperand.in - ): [string, ...string[]] => [valueTest(filterOperand)]; - - const query = getDefaultQuery(); - query.filter.target = { - id: { - [FilterOperand.eq]: valueTest(FilterOperand.eq), - [FilterOperand.ne]: valueTest(FilterOperand.ne), - }, - isActive: { - [FilterOperand.like]: valueTest(FilterOperand.like), - [FilterOperand.regexp]: valueTest(FilterOperand.regexp), - }, - firstName: { - [FilterOperand.gt]: valueTest(FilterOperand.gt), - [FilterOperand.gte]: valueTest(FilterOperand.gte), - }, - testDate: { - [FilterOperand.lt]: valueTest(FilterOperand.lt), - [FilterOperand.lte]: valueTest(FilterOperand.lt), - }, - createdAt: { - [FilterOperand.in]: valueTestArray(FilterOperand.in), - [FilterOperand.nin]: valueTestArray(FilterOperand.nin), - }, - }; - - function guardField( - filter: any, - field: any - ): asserts field is keyof R { - if (filter && !(field in filter)) - throw new Error('field not exist in query filter'); - } - - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - const mainAliasCheck = 'Users'; - const paramsNameSet = new Set(); - for (const item of result) { - const { params, alias, expression, selectInclude } = item; - expect(selectInclude).toBe(undefined); - if (!alias) { - expect(alias).not.toBe(undefined); - throw new Error('alias in undefined for result'); - } - const [mainAlias, field] = alias.split('.'); - expect(mainAlias).toBe(mainAliasCheck); - guardField(query.filter.target, field); - const filterName: any = query.filter.target[field]; - if (!filterName) { - expect(filterName).not.toBe(undefined); - throw new Error('filterName in undefined from query'); - } - if (!params) { - expect(params).not.toBe(undefined); - throw new Error('params in undefined for result'); - } - if (Array.isArray(params)) { - expect(params).not.toBeInstanceOf(Array); - throw new Error('params in undefined for result'); - } - const { val, name } = params; - expect(paramsNameSet.has(name)).toBe(false); - paramsNameSet.add(name); - const reg = new RegExp(`params_${alias}_\\d{1,}`); - const regResult = name.match(reg); - - if (regResult === null) { - expect(name.match(reg)).not.toBe(null); - throw new Error(`name is not pattern: params_${alias}_\\d{1,}`); - } - const expressionMap = expression.replace(name, EXPRESSION); - const checkFilterOperand = Object.entries(FilterOperand).find( - ([key, val]) => OperandsMapExpression[val] === expressionMap - ); - if (!checkFilterOperand) { - expect(checkFilterOperand).not.toBe(undefined); - throw new Error(`expression incorrect`); - } - - const operand = checkFilterOperand[0] as any; - guardField(filterName, operand); - if (operand === 'like') { - expect(params.val).toEqual(`%${filterName[operand]}%`); - } else { - expect(params.val).toEqual(filterName[operand]); - } - } - }); - it('expression for target relation field with relation column', () => { - const query = getDefaultQuery(); - query.filter.target = { - addresses: { - [FilterOperand.eq]: 'null', - [FilterOperand.ne]: 'null', - }, - }; - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - expect(result.length).toBe(2); - const [first, second] = result; - expect(first).not.toHaveProperty('params'); - expect(first).not.toHaveProperty('selectInclude'); - expect(first['alias']).toBe('Users.addresses'); - expect(first['expression']).toBe('IS NULL'); - expect(second).not.toHaveProperty('params'); - expect(second).not.toHaveProperty('selectInclude'); - expect(second['alias']).toBe('Users.addresses'); - expect(second['expression']).toBe('IS NOT NULL'); - }); - it('expression for target relation field with one-to-many', () => { - const query = getDefaultQuery(); - query.filter.target = { - comments: { - [FilterOperand.eq]: 'null', - [FilterOperand.ne]: 'null', - }, - }; - const subQuery = repositoryUser - .createQueryBuilder() - .subQuery() - .select('Comments.createdBy', 'createdBy') - .from(Comments, 'Comments') - .where(`Comments.createdBy = Users.id`) - .getQuery(); - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - expect(result.length).toBe(2); - const [first, second] = result; - expect(first).not.toHaveProperty('params'); - expect(first).not.toHaveProperty('selectInclude'); - expect(first).not.toHaveProperty('alias'); - expect(first['expression']).toBe(`NOT EXISTS ${subQuery}`); - expect(second).not.toHaveProperty('params'); - expect(second).not.toHaveProperty('selectInclude'); - expect(second).not.toHaveProperty('alias'); - expect(second['expression']).toBe(`EXISTS ${subQuery}`); - }); - it('expression for target relation field with many-to-many', () => { - const query = getDefaultQuery(); - query.filter.target = { - roles: { - [FilterOperand.eq]: 'null', - [FilterOperand.ne]: 'null', - }, - }; - const subQuery = getQuery() - .where(`Users-Roles.user_id = Users.id`) - .getQuery(); - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - - expect(result.length).toBe(2); - const [first, second] = result; - expect(first).not.toHaveProperty('params'); - expect(first).not.toHaveProperty('selectInclude'); - expect(first).not.toHaveProperty('alias'); - expect(first['expression']).toBe(`NOT EXISTS ${subQuery}`); - expect(second).not.toHaveProperty('params'); - expect(second).not.toHaveProperty('selectInclude'); - expect(second).not.toHaveProperty('alias'); - expect(second['expression']).toBe(`EXISTS ${subQuery}`); - }); - }); - - describe('getFilterExpressionForRelation', () => { - it('expression for relation many-to-many', () => { - const query = getDefaultQuery(); - const conditional = { - name: { - [FilterOperand.eq]: 'null', - [FilterOperand.ne]: 'null', - }, - createdAt: { - [FilterOperand.eq]: 'test1', - [FilterOperand.ne]: 'test2', - [FilterOperand.nin]: ['test3'] as [string, ...string[]], - }, - }; - - query.filter.relation = { - roles: conditional, - }; - - let subQuery = getQuery() - .where(`"Users__Roles_roles"."name" IS NULL`) - .andWhere(`"Users__Roles_roles"."name" IS NOT NULL`) - .andWhere(`"Users__Roles_roles"."created_at" = :param1`) - .andWhere(`"Users__Roles_roles"."created_at" <> :param2`) - .andWhere(`"Users__Roles_roles"."created_at" NOT IN (:...param3)`) - .getQuery(); - - const result = - typeormUtilsServiceUser.getFilterExpressionForRelation(query); - - expect(result.length).toBe(1); - - const [first] = result; - expect(first).not.toHaveProperty('selectInclude'); - if (!first.params && !Array.isArray(first.params)) { - expect(first).toHaveProperty('params'); - expect(first.params).toBeInstanceOf(Array); - } - if (Array.isArray(first.params)) { - expect(first?.params?.length).toBe(3); - const [firstParams, secondParams, thirdParams] = first.params; - expect(firstParams?.val).toBe( - query.filter?.relation?.roles?.createdAt?.eq - ); - - const regResult1 = firstParams?.name.match( - new RegExp(`params_Roles.createdAt_\\d{1,}`) - ); - if (regResult1) { - subQuery = subQuery.replace('param1', regResult1[0]); - } - expect(regResult1).not.toBe(null); - - expect(secondParams?.val).toBe( - query.filter?.relation?.roles?.createdAt?.ne - ); - - const regResult2 = secondParams?.name.match( - new RegExp(`params_Roles.createdAt_\\d{1,}`) - ); - if (regResult2) { - subQuery = subQuery.replace('param2', regResult2[0]); - } - expect(regResult2).not.toBe(null); - - expect(thirdParams?.val).toBe( - query.filter?.relation?.roles?.createdAt?.nin - ); - const regResult3 = thirdParams?.name.match( - new RegExp(`params_Roles.createdAt_\\d{1,}`) - ); - if (regResult3) { - subQuery = subQuery.replace('param3', regResult3[0]); - } - } - expect(first.alias).toBe(`Users.id`); - expect(first.expression).toBe(`IN ${subQuery}`); - }); - - it('expression for relation other type', () => { - const query = getDefaultQuery(); - query.filter.relation = { - addresses: { - createdAt: { - eq: 'qweqwe', - }, - }, - comments: { - createdAt: { - like: 'sdfsdf', - }, - }, - }; - const firstAlias = 'Addresses.createdAt'; - const secondAlias = 'Comments.createdAt'; - const result = - typeormUtilsServiceUser.getFilterExpressionForRelation(query); - - expect(result.length).toBe(2); - const [first, second] = result; - - const firstResult = first.expression.match( - new RegExp(`params_${firstAlias}_\\d{1,}`) - ); - - if (!firstResult) { - expect(firstResult).not.toBe(null); - throw Error('Should be like pattern'); - } - - expect(first.expression).toBe(`= :${firstResult[0]}`); - expect(first.alias).toBe(`Users__Addresses_addresses.createdAt`); - expect(first.selectInclude).toBe('addresses'); - if (!Array.isArray(first.params)) { - expect(first.params?.name).toBe(`${firstResult[0]}`); - expect(first.params?.val).toBe( - query.filter.relation?.addresses?.createdAt?.eq - ); - } else { - expect(first.params).not.toBeInstanceOf(Array); - } - - const secondResult = second.expression.match( - new RegExp(`params_${secondAlias}_\\d{1,}`) - ); - if (!secondResult) { - expect(secondResult).not.toBe(null); - throw Error('Should be like pattern'); - } - - expect(second.expression).toBe(`ILIKE :${secondResult[0]}`); - expect(second.alias).toBe('Users__Comments_comments.createdAt'); - expect(second.selectInclude).toBe('comments'); - if (!Array.isArray(second.params)) { - expect(second.params?.name).toBe(secondResult[0]); - expect(second.params?.val).toBe( - `%${query.filter.relation?.comments?.createdAt?.like}%` - ); - } else { - expect(second.params).not.toBeInstanceOf(Array); - } - }); - }); - - describe('validateRelationInputData', () => { - let usersData: Users; - beforeEach(async () => { - const result = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - roles: true, - userGroup: true, - manager: true, - }, - }); - if (!result) { - throw Error('not found mock data'); - } - usersData = result; - }); - it('should be ok', async () => { - const rolesData = usersData.roles.map((i) => ({ - type: 'roles', - id: i.id.toString(), - })); - - const userGroupData = { - type: 'user-groups', - id: usersData.userGroup.id.toString(), - }; - const managerData = { - type: 'users', - id: usersData.manager.id.toString(), - }; - const emptyRoles: { id: string; type: string }[] = []; - const emptyManager = null; - const result = await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - rolesData - ); - const result1 = await typeormUtilsServiceUser.validateRelationInputData( - 'userGroup', - userGroupData - ); - const result2 = await typeormUtilsServiceUser.validateRelationInputData( - 'manager', - managerData - ); - const result3 = await typeormUtilsServiceUser.validateRelationInputData( - 'manager', - emptyManager - ); - const result4 = await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - emptyRoles - ); - expect(result).toEqual(usersData.roles.map((i) => i.id.toString())); - expect(result1).toEqual(usersData.userGroup.id.toString()); - expect(result2).toEqual(usersData.manager.id.toString()); - expect(result3).toEqual(emptyManager); - expect(result4).toEqual(emptyRoles); - }); - - it('Should be error incorrect type name', async () => { - const rolesData = usersData.roles.map((i, index) => ({ - type: index === 1 ? 'other' : 'roles', - id: i.id.toString(), - })) as PostRelationshipData; - - const userGroupData = { - type: 'userGroups', - id: usersData.userGroup.id.toString(), - }; - const managerData = { - type: 'user', - id: usersData.manager.id.toString(), - }; - expect.assertions(3); - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - rolesData - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'userGroup', - userGroupData - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'manager', - managerData - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - }); - - it('Should be error, Incorrect relation type', async () => { - expect.assertions(2); - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - {} as any - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'userGroup', - [] as any - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - }); - - it('Should be error, Not fond', async () => { - const rolesData = usersData.roles.map((i, index) => ({ - type: 'roles', - id: index === 1 ? '1000' : i.id.toString(), - })) as PostRelationshipData; - expect.assertions(2); - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - rolesData - ); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - try { - await typeormUtilsServiceUser.validateRelationInputData('userGroup', { - type: 'user-groups', - id: '10000', - }); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts deleted file mode 100644 index 846a575e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts +++ /dev/null @@ -1,678 +0,0 @@ -import { - BadRequestException, - Inject, - Injectable, - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; -import { EntityMetadata, Equal, In, Repository } from 'typeorm'; -import { RelationMetadata as TypeOrmRelationMetadata } from 'typeorm/metadata/RelationMetadata'; - -import { - Entity, - EntityRelation, - EXPRESSION, - FilterOperand, - OperandMapExpressionForNull, - OperandsMapExpression, - OperandsMapExpressionForNullRelation, - ValidateQueryError, -} from '../../types'; -import { - camelToKebab, - getEntityName, - kebabToCamel, - ObjectTyped, - snakeToCamel, -} from '../../helper/utils'; -import { PatchData, PostData, Query } from '../../helper/zod'; -import { CURRENT_ENTITY_REPOSITORY } from '../../constants'; -import { TupleOfEntityRelation } from '../../helper/orm'; - -type RelationAlias = { - [K in TupleOfEntityRelation[number]]: string; -}; -type RelationMetadata = { - [K in TupleOfEntityRelation[number]]: TypeOrmRelationMetadata; -}; - -type ResultQueryExpressionObject = { name: string; val: string }; -type ResultQueryExpressionArray = { name: string; val: string }[]; - -export type RelationshipsResult = { - [K in EntityRelation]: E[K] extends E[K][] ? E[K] : E[K] | null; -}; - -export type ResultQueryExpression = { - alias?: string; - expression: string; - paramsForResult?: string[]; - params?: ResultQueryExpressionObject | ResultQueryExpressionArray; - selectInclude?: string; -}; -export type InputValidateData = { - type: string; - id: string; -}; - -export type ValidateReturn = T extends unknown[] - ? string[] - : T extends null - ? null - : string; - -function isTargetField( - relationField: TupleOfEntityRelation, - field: any -): field is TupleOfEntityRelation[number] { - return relationField.includes(field); -} - -function isRelationField( - relationField: TupleOfEntityRelation, - field: any -): asserts field is EntityRelation { - if (isTargetField(relationField, field)) return; - const error: ValidateQueryError = { - code: 'unrecognized_keys', - path: ['data', 'relationships'], - message: `Resource for relation '${field.toString()}' does not exist`, - keys: [field], - }; - - throw new BadRequestException([error]); -} - -Injectable(); -export class TypeormUtilsService { - private readonly _currentAlias!: string; - private readonly _relationMetadata = {} as RelationMetadata; - private readonly _relationAlias = {} as RelationAlias; - private readonly _relationFields!: TupleOfEntityRelation; - private readonly _entityMetadata!: EntityMetadata; - private _number = 0; - - constructor( - @Inject(CURRENT_ENTITY_REPOSITORY) private repository: Repository - ) { - this._currentAlias = snakeToCamel(repository.metadata.name); - const relationFields = []; - for (const metadata of repository.metadata.relations) { - const propertyName = - metadata.propertyName as TupleOfEntityRelation[number]; - this._relationMetadata[propertyName] = metadata; - this._relationAlias[propertyName] = snakeToCamel( - metadata.inverseEntityMetadata.name - ); - relationFields.push(propertyName); - } - this._relationFields = relationFields as TupleOfEntityRelation; - this._entityMetadata = repository.metadata; - } - get currentAlias() { - return this._currentAlias; - } - - get relationFields() { - return this._relationFields; - } - - relationName(relName: TupleOfEntityRelation[number]) { - return this._relationAlias[relName]; - } - - get currentPrimaryColumn(): keyof E { - return this._entityMetadata.primaryColumns[0].propertyName as keyof E; - } - - getAliasForRelation(relName: TupleOfEntityRelation[number]) { - return `${this.currentAlias}__${this._relationAlias[relName]}_${relName}`; - } - - getRelMetaDataForRelation(relName: TupleOfEntityRelation[number]) { - return this._relationMetadata[relName]; - } - - getPrimaryColumnForRel(relName: TupleOfEntityRelation[number]) { - return this._relationMetadata[relName].inverseEntityMetadata - .primaryColumns[0].propertyName; - } - - private getFilterObject(query: Query, filterType: 'target' | 'relation') { - const { filter } = query; - if (!filter) return null; - return filter[filterType]; - } - - private get number() { - if (this._number > 1000) { - this._number = 0; - } - this._number++; - return this._number; - } - - private getParamName(fieldName: string) { - return `params_${fieldName}_${this.number}`; - } - - getAliasPath(fieldName: unknown): string; - getAliasPath( - fieldName: unknown, - relname: TupleOfEntityRelation[number], - separator?: string - ): string; - getAliasPath(fieldName: unknown, relname: string, separator?: string): string; - getAliasPath( - fieldName: unknown, - relname?: TupleOfEntityRelation[number] | string, - separator = '.' - ): string { - const alias = relname - ? this._relationAlias[relname] || relname - : this.currentAlias; - return `${alias}${separator}${fieldName}`; - } - - private getSubQueryForManyToMany( - fieldName: TupleOfEntityRelation[number], - expression?: string[] - ): string { - const metadataRelation: TypeOrmRelationMetadata = - this._relationMetadata[fieldName]; - const relationPrimaryColumn = - metadataRelation.inverseEntityMetadata.primaryColumns[0].propertyName; - const { joinTableName, inverseJoinColumns, joinColumns } = - metadataRelation.isManyToManyOwner - ? metadataRelation - : metadataRelation.inverseRelation || metadataRelation; - - const { databaseName: queryJoinPropsName } = - metadataRelation.isManyToManyOwner - ? inverseJoinColumns[0] - : joinColumns[0]; - const { databaseName: selectJoinPropsName } = - metadataRelation.isManyToManyOwner - ? joinColumns[0] - : inverseJoinColumns[0]; - - const alias = this.getAliasPath( - this._relationAlias[fieldName], - this.currentAlias, - '-' - ); - - const selectAlias = this.getAliasPath(selectJoinPropsName, alias); - - const query = this.repository - .createQueryBuilder() - .subQuery() - .select(selectAlias) - .from(joinTableName, alias) - .leftJoin( - this._relationMetadata[fieldName].inverseEntityMetadata.target, - this.getAliasForRelation(fieldName), - `${this.getAliasPath(queryJoinPropsName, alias)} = ${this.getAliasPath( - relationPrimaryColumn, - this.getAliasForRelation(fieldName) - )}` - ); - if (!expression) { - query.where( - `${selectAlias} = ${this.getAliasPath(this.currentPrimaryColumn)}` - ); - } else { - for (const i in expression) { - query[i === '0' ? 'where' : 'andWhere'](expression[i]); - } - } - return query.getQuery(); - } - - getFilterExpressionForTarget(query: Query): ResultQueryExpression[] { - const resultExpression: ResultQueryExpression[] = []; - const filterTarget = this.getFilterObject(query, 'target'); - if (!filterTarget) return resultExpression; - for (const [fieldName, filter] of ObjectTyped.entries(filterTarget)) { - if (!filter) continue; - for (const entries of ObjectTyped.entries(filter)) { - const [operand, value] = entries as [FilterOperand, string]; - const valueConditional = - operand === FilterOperand.like ? `%${value}%` : value; - const fieldWithAlias = this.getAliasPath(fieldName); - const paramsName = this.getParamName(fieldWithAlias); - - if (!isTargetField(this._relationFields, fieldName)) { - if ( - (operand === FilterOperand.ne || operand === FilterOperand.eq) && - (valueConditional === 'null' || valueConditional === null) - ) { - const expression = OperandMapExpressionForNull[operand].replace( - EXPRESSION, - paramsName - ); - resultExpression.push({ - alias: fieldWithAlias, - expression, - }); - continue; - } - - const expression = OperandsMapExpression[operand].replace( - EXPRESSION, - paramsName - ); - resultExpression.push({ - alias: fieldWithAlias, - expression, - params: { - val: valueConditional, - name: paramsName, - }, - }); - continue; - } - - const metadataRelation: TypeOrmRelationMetadata = - this._relationMetadata[fieldName]; - const relationTarget = metadataRelation.inverseEntityMetadata.target; - const relationAlias = this._relationAlias[fieldName]; - const subQuery = this.repository.createQueryBuilder().subQuery(); - - const resultOperand = - operand === FilterOperand.eq ? operand : FilterOperand.ne; - switch (metadataRelation.relationType) { - case 'many-to-many': { - const subQuerySql = this.getSubQueryForManyToMany(fieldName); - - const resultOperand = - operand === FilterOperand.eq ? operand : FilterOperand.ne; - - const expression = OperandsMapExpressionForNullRelation[ - resultOperand - ].replace(EXPRESSION, subQuerySql); - - resultExpression.push({ - expression, - }); - break; - } - case 'one-to-many': { - const joinColumn = metadataRelation.inverseSidePropertyPath; - - const aliasPath = this.getAliasPath(joinColumn, fieldName); - const subQuerySql = subQuery - .select(aliasPath, joinColumn) - .from(relationTarget, relationAlias) - .where( - `${aliasPath} = ${this.getAliasPath(this.currentPrimaryColumn)}` - ) - .getQuery(); - - const expression = OperandsMapExpressionForNullRelation[ - resultOperand - ].replace(EXPRESSION, subQuerySql); - - resultExpression.push({ - expression, - }); - break; - } - default: { - const expression = OperandMapExpressionForNull[ - resultOperand - ].replace(EXPRESSION, paramsName); - resultExpression.push({ - alias: fieldWithAlias, - expression, - }); - } - } - } - } - - return resultExpression; - } - - getFilterExpressionForRelation(query: Query): ResultQueryExpression[] { - const resultExpression: ResultQueryExpression[] = []; - const filterRelation = this.getFilterObject(query, 'relation'); - if (!filterRelation) return resultExpression; - - for (const [relationField, propsFilter] of ObjectTyped.entries( - filterRelation - )) { - if (!propsFilter) continue; - if (!isTargetField(this._relationFields, relationField)) continue; - const metadataRelation: TypeOrmRelationMetadata = - this._relationMetadata[relationField]; - - const conditionalForManyToMany: { - conditional: string; - params: { name: string; val: string } | undefined; - }[] = []; - - for (const [relationFieldProps, filter] of ObjectTyped.entries( - propsFilter - )) { - if (!filter) continue; - - for (const entries of ObjectTyped.entries(filter)) { - const [operand, value] = entries as [FilterOperand, string]; - const currentValue = - operand === FilterOperand.like ? `%${value}%` : value; - - const paramsName = this.getParamName( - this.getAliasPath(relationFieldProps.toString(), relationField) - ); - let expression: string; - if (value === 'null') { - const currentOperand = - operand === FilterOperand.eq - ? FilterOperand.eq - : FilterOperand.ne; - expression = OperandMapExpressionForNull[currentOperand]; - } else { - expression = OperandsMapExpression[operand].replace( - EXPRESSION, - paramsName - ); - } - - const params = - value === 'null' - ? undefined - : { - val: currentValue, - name: paramsName, - }; - - switch (metadataRelation.relationType) { - case 'many-to-many': { - conditionalForManyToMany.push({ - params, - conditional: `${this.getAliasPath( - relationFieldProps.toString(), - this.getAliasForRelation(relationField) - )} ${expression}`, - }); - - break; - } - default: { - resultExpression.push({ - alias: this.getAliasPath( - relationFieldProps.toString(), - this.getAliasForRelation(relationField) - ), - expression, - selectInclude: relationField, - params, - }); - } - } - } - } - - if (conditionalForManyToMany.length) { - const expression = conditionalForManyToMany.map((i) => i.conditional); - const subQuery = this.getSubQueryForManyToMany( - relationField, - expression - ); - - const mainExpression = `IN ${subQuery}`; - - const params = conditionalForManyToMany - .filter((i) => i.params) - .map((i) => i.params) as { name: string; val: string }[]; - resultExpression.push({ - alias: this.getAliasPath(this.currentPrimaryColumn), - expression: mainExpression, - paramsForResult: expression, - params, - }); - } - } - return resultExpression; - } - - private throwError(message: string, path: string[], key?: string) { - const error: ValidateQueryError = { - code: 'unrecognized_keys', - path, - message, - }; - if (key) { - error.keys = [key]; - } - throw new BadRequestException([error]); - } - - async *asyncIterateFindRelationships( - relationships: NonNullable< - PatchData['relationships'] | PostData['relationships'] - > - ): AsyncGenerator> { - for (const entries of ObjectTyped.entries(relationships)) { - const [props, data] = entries; - - isRelationField(this._relationFields, props); - if (data === undefined) continue; - - if (data === null) { - yield { [props]: null } as RelationshipsResult; - continue; - } - const isArray = Array.isArray(data); - if (isArray && data.length === 0) { - yield { [props]: [] } as RelationshipsResult; - continue; - } - const condition = isArray ? In(data.map((i) => i.id)) : Equal(data['id']); - const relationsTypeName = kebabToCamel( - isArray ? data[0]['type'] : data['type'] - ); - const primaryField = this.getPrimaryColumnForRel( - props as TupleOfEntityRelation[number] - ); - const relationsTarget = - this._relationMetadata[props as TupleOfEntityRelation[number]] - .inverseEntityMetadata.target; - const result = await this.repository.manager - .getRepository(relationsTarget) - .find({ - select: { - [primaryField]: true, - }, - where: { - [primaryField]: condition, - }, - }); - - if ( - (isArray && (result.length === 0 || data.length !== result.length)) || - (!isArray && result.length === 0) - ) { - const message = isArray - ? `Resource '${relationsTypeName}' with ids '${data - .map((i) => i.id) - .filter((i) => !result.find((r) => r[primaryField] == i)) - .join(',')}' does not exist` - : `Resource '${relationsTypeName}' with id '${data.id}' does not exist`; - - const error: ValidateQueryError = { - code: 'invalid_arguments', - path: ['data', 'relationships', props.toString()], - message, - }; - - throw new BadRequestException([error]); - } - - yield { [props]: isArray ? result : result[0] } as RelationshipsResult; - } - } - - async saveEntityData( - target: E, - relationships: PatchData['relationships'] | PostData['relationships'] - ): Promise { - if (relationships) { - for await (const item of this.asyncIterateFindRelationships( - relationships - )) { - const [props, type] = ObjectTyped.entries(item)[0]; - if (type !== null) { - target[props] = type as any; - } else { - target[props] = null as any; - } - } - } - const saveData = await this.repository.save(target); - let saveDataWithRelation: E | null = null; - if (relationships) { - const queryBuilder = this.repository - .createQueryBuilder(this.currentAlias) - .where({ - [this.currentPrimaryColumn]: Equal( - saveData[this.currentPrimaryColumn] - ), - }); - - for (const [props] of ObjectTyped.entries(relationships)) { - const currentIncludeAlias = this.getAliasForRelation(props.toString()); - - queryBuilder.leftJoinAndSelect( - this.getAliasPath(props), - currentIncludeAlias - ); - } - - saveDataWithRelation = await queryBuilder.getOne(); - } - - return saveDataWithRelation ? saveDataWithRelation : saveData; - } - - async validateRelationInputData< - Rel extends EntityRelation, - In extends InputValidateData | InputValidateData[] - >(rel: Rel, inputData: In): Promise>; - async validateRelationInputData< - Rel extends EntityRelation, - In extends InputValidateData | InputValidateData[] - >(rel: Rel, inputData: In): Promise>; - async validateRelationInputData< - Rel extends EntityRelation, - In extends null | InputValidateData | InputValidateData[] - >(rel: Rel, inputData: In): Promise>; - async validateRelationInputData< - Rel extends EntityRelation, - In extends null | InputValidateData | InputValidateData[] - >(rel: Rel, inputData: In): Promise> { - const relationMetadata = - this._relationMetadata[rel as TupleOfEntityRelation[number]]; - const isArray = Array.isArray(inputData); - - if ( - ['one-to-many', 'many-to-many'].includes(relationMetadata.relationType) && - !isArray - ) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - path: ['data'], - message: 'Body data should be array', - }; - - throw new UnprocessableEntityException([error]); - } - - if ( - ['one-to-one', 'many-to-one'].includes(relationMetadata.relationType) && - isArray - ) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - path: ['data'], - message: 'Body data should be object', - }; - - throw new UnprocessableEntityException([error]); - } - if (inputData === null) { - const result = null; - return result as ValidateReturn; - } - if (isArray && inputData.length === 0) { - const result: any[] = []; - return result as ValidateReturn; - } - - const prepareData = isArray ? inputData : [inputData]; - - const errors: ValidateQueryError[] = []; - let i = 0; - const typeName = camelToKebab( - getEntityName(relationMetadata.inverseEntityMetadata.target) - ); - - for (const prepareItem of prepareData) { - if (prepareItem.type !== typeName) { - const path = isArray ? ['data', i.toString()] : ['data']; - errors.push({ - code: 'invalid_arguments', - path: path, - message: `Type should be equal to type of relName: "${rel.toString()}". Type of ${rel.toString()} is "${typeName}" but receive - "${ - prepareItem.type - }"`, - }); - } - i++; - } - if (errors.length) { - throw new UnprocessableEntityException(errors); - } - - const checkResult = await this.repository.manager - .getRepository(relationMetadata.inverseEntityMetadata.target) - .find({ - select: { - [this.getPrimaryColumnForRel(rel.toString())]: true, - }, - where: { - [this.getPrimaryColumnForRel(rel.toString())]: In( - prepareData.map((i) => i.id) - ), - }, - }); - - if (checkResult.length === prepareData.length) { - return ( - isArray ? inputData.map((i) => i.id) : inputData.id - ) as ValidateReturn; - } - - const resulDataMap = checkResult.reduce((acum, item) => { - acum[item[this.getPrimaryColumnForRel(rel.toString())]] = true; - return acum; - }, {} as Record); - - i = 0; - for (const item of prepareData) { - if (!resulDataMap[item.id]) { - const path = isArray ? ['data', i.toString(), 'id'] : ['data', 'id']; - errors.push({ - code: 'invalid_arguments', - path: path, - message: `Not exist item "${ - item.id - }" in relation "${rel.toString()}"`, - }); - } - i++; - } - throw new NotFoundException(errors); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test b/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test deleted file mode 100644 index fa08bc14..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test +++ /dev/null @@ -1,647 +0,0 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 12.5 --- Dumped by pg_dump version 12.5 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -create extension "uuid-ossp"; - --- --- Name: comment_kind_enum; Type: TYPE; Schema: public; Owner: - --- - -CREATE TYPE public.comment_kind_enum AS ENUM ( - 'COMMENT', - 'MESSAGE', - 'NOTE' -); - - -SET default_table_access_method = heap; - --- --- Name: addresses; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.addresses ( - id integer NOT NULL, - city character varying(70) DEFAULT NULL::character varying, - state character varying(70) DEFAULT NULL::character varying, - country character varying(70) DEFAULT NULL::character varying, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - array_field text[] -); - - --- --- Name: addresses_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.addresses_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: addresses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.addresses_id_seq OWNED BY public.addresses.id; - - --- --- Name: comments; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.comments ( - id integer NOT NULL, - text text NOT NULL, - kind public.comment_kind_enum NOT NULL, - created_by integer, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: comments_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.comments_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.comments_id_seq OWNED BY public.comments.id; - - --- --- Name: notes; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.notes ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - text text NOT NULL, - created_by integer, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - - --- --- Name: migrations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.migrations ( - id integer NOT NULL, - "timestamp" bigint NOT NULL, - name character varying NOT NULL -); - - --- --- Name: migrations_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.migrations_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: migrations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.migrations_id_seq OWNED BY public.migrations.id; - - --- --- Name: pods; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.pods ( - id integer NOT NULL, - name character varying, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: pods_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.pods_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: pods_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.pods_id_seq OWNED BY public.pods.id; - - --- --- Name: requests; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.requests ( - id integer NOT NULL, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: requests_have_pod_locks; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.requests_have_pod_locks ( - id integer NOT NULL, - request_id integer NOT NULL, - pod_id integer NOT NULL, - external_id integer, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: requests_have_pod_locks_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.requests_have_pod_locks_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: requests_have_pod_locks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.requests_have_pod_locks_id_seq OWNED BY public.requests_have_pod_locks.id; - - --- --- Name: requests_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.requests_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: requests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.requests_id_seq OWNED BY public.requests.id; - - --- --- Name: roles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.roles ( - id integer NOT NULL, - name character varying(128) DEFAULT NULL::character varying, - key character varying(128) NOT NULL, - is_default boolean DEFAULT false, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.roles_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.roles_id_seq OWNED BY public.roles.id; - - --- --- Name: typeorm_metadata; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.typeorm_metadata ( - type character varying NOT NULL, - database character varying, - schema character varying, - "table" character varying, - name character varying, - value text -); - - --- --- Name: users; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.users ( - id integer NOT NULL, - login character varying(100) NOT NULL, - first_name character varying, - last_name character varying, - is_active boolean DEFAULT false, - test_real real[], - test_array_null real[], - manager_id integer, - addresses_id integer, - user_groups_id integer, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - test_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - --- --- Name: user_groups; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.user_groups ( - id integer NOT NULL, - label character varying NOT NULL -); - - --- --- Name: users_have_roles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.users_have_roles ( - id integer NOT NULL, - user_id integer NOT NULL, - role_id integer NOT NULL, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: users_have_roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.users_have_roles_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: users_have_roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.users_have_roles_id_seq OWNED BY public.users_have_roles.id; - - --- --- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.users_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; - - --- --- Name: user_groups_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.user_groups_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: user_groups_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.user_groups_id_seq OWNED BY public.user_groups.id; - - --- --- Name: addresses id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.addresses ALTER COLUMN id SET DEFAULT nextval('public.addresses_id_seq'::regclass); - - --- --- Name: comments id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comments ALTER COLUMN id SET DEFAULT nextval('public.comments_id_seq'::regclass); - - --- --- Name: migrations id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migrations ALTER COLUMN id SET DEFAULT nextval('public.migrations_id_seq'::regclass); - - --- --- Name: pods id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.pods ALTER COLUMN id SET DEFAULT nextval('public.pods_id_seq'::regclass); - - --- --- Name: requests id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests ALTER COLUMN id SET DEFAULT nextval('public.requests_id_seq'::regclass); - - --- --- Name: requests_have_pod_locks id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests_have_pod_locks ALTER COLUMN id SET DEFAULT nextval('public.requests_have_pod_locks_id_seq'::regclass); - - --- --- Name: roles id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.roles ALTER COLUMN id SET DEFAULT nextval('public.roles_id_seq'::regclass); - - --- --- Name: users id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); - - --- --- Name: users id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.user_groups ALTER COLUMN id SET DEFAULT nextval('public.user_groups_id_seq'::regclass); - - --- --- Name: users_have_roles id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users_have_roles ALTER COLUMN id SET DEFAULT nextval('public.users_have_roles_id_seq'::regclass); - - --- --- Name: requests PK_0428f484e96f9e6a55955f29b5f; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests - ADD CONSTRAINT "PK_0428f484e96f9e6a55955f29b5f" PRIMARY KEY (id); - - --- --- Name: addresses PK_745d8f43d3af10ab8247465e450; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.addresses - ADD CONSTRAINT "PK_745d8f43d3af10ab8247465e450" PRIMARY KEY (id); - - --- --- Name: comments PK_8bf68bc960f2b69e818bdb90dcb; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comments - ADD CONSTRAINT "PK_8bf68bc960f2b69e818bdb90dcb" PRIMARY KEY (id); - - --- --- Name: migrations PK_8c82d7f526340ab734260ea46be; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migrations - ADD CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id); - - --- --- Name: users_have_roles PK_9bb88c2f9f64bff7570e4108108; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users_have_roles - ADD CONSTRAINT "PK_9bb88c2f9f64bff7570e4108108" PRIMARY KEY (id); - - --- --- Name: users PK_a3ffb1c0c8416b9fc6f907b7433; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY (id); - - --- --- Name: users PK_user_groups; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.user_groups - ADD CONSTRAINT "PK_user_groups" PRIMARY KEY (id); - --- --- Name: pods PK_b00bbc2c7fb41627be2b169f0dd; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.pods - ADD CONSTRAINT "PK_b00bbc2c7fb41627be2b169f0dd" PRIMARY KEY (id); - - --- --- Name: roles PK_c1433d71a4838793a49dcad46ab; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.roles - ADD CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY (id); - - --- --- Name: requests_have_pod_locks PK_f214657396a396b70a697b04a85; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests_have_pod_locks - ADD CONSTRAINT "PK_f214657396a396b70a697b04a85" PRIMARY KEY (id); - - --- --- Name: users UQ_2d443082eccd5198f95f2a36e2c; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "UQ_2d443082eccd5198f95f2a36e2c" UNIQUE (login); - - --- --- Name: roles UQ_a87cf0659c3ac379b339acf36a2; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.roles - ADD CONSTRAINT "UQ_a87cf0659c3ac379b339acf36a2" UNIQUE (key); - - --- --- Name: IDX_48d6a9a1ab3943e6c6d2a25d2e; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX "IDX_48d6a9a1ab3943e6c6d2a25d2e" ON public.requests_have_pod_locks USING btree (request_id, pod_id); - - --- --- Name: IDX_61c360686dfe8d62a9b03873bf; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX "IDX_61c360686dfe8d62a9b03873bf" ON public.users_have_roles USING btree (user_id, role_id); - - --- --- Name: users FK_2f8d527df0d3acb8aa51945a968; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "FK_2f8d527df0d3acb8aa51945a968" FOREIGN KEY (addresses_id) REFERENCES public.addresses(id); - - --- --- Name: users FK_user_groups; Type: FK CONSTRAINT; Schema: public; Owner: - --- -ALTER TABLE ONLY public.users - ADD CONSTRAINT "FK_user_groups" FOREIGN KEY (user_groups_id) REFERENCES public.user_groups(id); - - --- --- Name: users_have_roles FK_6e768e03083247102b401b74b46; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users_have_roles - ADD CONSTRAINT "FK_6e768e03083247102b401b74b46" FOREIGN KEY (role_id) REFERENCES public.roles(id); - - --- --- Name: comments FK_980bfefe00ed11685f325d0bd4c; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comments - ADD CONSTRAINT "FK_980bfefe00ed11685f325d0bd4c" FOREIGN KEY (created_by) REFERENCES public.users(id); - - --- --- Name: notes FK_980bfefe00ed11685f325d0bd4c; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.notes - ADD CONSTRAINT "FK_notes" FOREIGN KEY (created_by) REFERENCES public.users(id); - - --- --- Name: requests_have_pod_locks FK_c7531fe6bbb926bba3f69fcbb55; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests_have_pod_locks - ADD CONSTRAINT "FK_c7531fe6bbb926bba3f69fcbb55" FOREIGN KEY (pod_id) REFERENCES public.pods(id); - - --- --- Name: users_have_roles FK_df6a0246fcd887dd8ffeed2c292; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users_have_roles - ADD CONSTRAINT "FK_df6a0246fcd887dd8ffeed2c292" FOREIGN KEY (user_id) REFERENCES public.users(id); - - --- --- Name: requests_have_pod_locks FK_f3729b493fcdb7309cad08837ff; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests_have_pod_locks - ADD CONSTRAINT "FK_f3729b493fcdb7309cad08837ff" FOREIGN KEY (request_id) REFERENCES public.requests(id); - - --- --- Name: users FK_fba2d8e029689aa8fea98e53c91; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "FK_fba2d8e029689aa8fea98e53c91" FOREIGN KEY (manager_id) REFERENCES public.users(id); - - --- --- PostgreSQL database dump complete --- diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts deleted file mode 100644 index 3f0c7dab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - PrimaryGeneratedColumn, - OneToOne, - Column, - Entity, - UpdateDateColumn, -} from 'typeorm'; - -import { Users, IUsers } from './index'; - -export type IAddresses = Addresses; - -@Entity('addresses') -export class Addresses { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'varchar', - length: 70, - nullable: true, - default: 'NULL', - }) - public city!: string; - - @Column({ - type: 'varchar', - length: 70, - nullable: true, - default: 'NULL', - }) - public state!: string; - - @Column({ - type: 'varchar', - length: 68, - nullable: true, - default: 'NULL', - }) - public country!: string; - - @Column({ - name: 'array_field', - type: 'varchar', - nullable: true, - default: 'NULL', - array: true, - }) - public arrayField!: string[]; - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @OneToOne(() => Users, (item) => item.addresses) - public user!: IUsers; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts deleted file mode 100644 index c6f3ff8e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - PrimaryGeneratedColumn, - Column, - Entity, - JoinColumn, - ManyToOne, - UpdateDateColumn, -} from 'typeorm'; - -export enum CommentKind { - Comment = 'COMMENT', - Message = 'MESSAGE', - Note = 'NOTE', -} - -import { Users, IUsers } from './index'; - -@Entity('comments') -export class Comments { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'text', - nullable: false, - }) - public text!: string; - - @Column({ - type: 'enum', - enum: CommentKind, - nullable: false, - }) - public kind!: CommentKind; - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToOne(() => Users, (item) => item.id) - @JoinColumn({ - name: 'created_by', - }) - public createdBy!: IUsers; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts deleted file mode 100644 index ba42e083..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -export * from './users'; -export * from './roles'; -export * from './requests-have-pod-locks'; -export * from './requests'; -export * from './pods'; -export * from './comments'; -export * from './addresses'; -export * from './user-groups'; -export * from './notes'; - -import { Users } from './users'; -import { Roles } from './roles'; -import { Requests } from './requests'; -import { Pods } from './pods'; -import { Comments } from './comments'; -import { Addresses } from './addresses'; -import { UserGroups } from './user-groups'; -import { Notes } from './notes'; - -export const Entities = [ - Users, - Roles, - Requests, - Pods, - Comments, - Addresses, - UserGroups, - Notes, -]; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts deleted file mode 100644 index e8694aca..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - PrimaryGeneratedColumn, - Column, - Entity, - JoinColumn, - ManyToOne, - UpdateDateColumn, -} from 'typeorm'; - -import { Users, IUsers } from './index'; - -@Entity('notes') -export class Notes { - @PrimaryGeneratedColumn('uuid') - public id!: string; - - @Column({ - type: 'text', - nullable: false, - }) - public text!: string; - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToOne(() => Users, (item) => item.notes) - @JoinColumn({ - name: 'created_by', - }) - public createdBy!: IUsers; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts deleted file mode 100644 index b5fb898f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - ManyToMany, - PrimaryColumn, - UpdateDateColumn, -} from 'typeorm'; - -import { IRequests, Requests } from './index'; - -export type IPods = Pods; - -@Entity('pods') -export class Pods { - @PrimaryColumn() - public id!: string; - - @Column({ - type: 'varchar', - length: 50, - nullable: false, - unique: true, - }) - public name!: string; - - @CreateDateColumn({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToMany(() => Requests, (item) => item.podLocks) - public lockedRequests!: IRequests[]; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts deleted file mode 100644 index 7f1f8125..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - AfterLoad, - BeforeInsert, - BeforeRemove, - BeforeUpdate, - Column, - CreateDateColumn, - Entity, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; - -export type IRequestsHavePodLocks = RequestsHavePodLocks; - -@Entity('requests_have_pod_locks') -export class RequestsHavePodLocks { - @PrimaryGeneratedColumn() - public id!: number; - - @AfterLoad() - protected getRequestId() { - this.requestId = this.request_id; - } - - @BeforeInsert() - @BeforeUpdate() - @BeforeRemove() - protected setRequestId() { - if (this.requestId) { - this.request_id = this.requestId; - } - } - - public requestId!: number; - - @AfterLoad() - protected getPodId() { - this.podId = this.pod_id; - } - - @BeforeInsert() - @BeforeUpdate() - @BeforeRemove() - protected setPodId() { - if (this.podId) { - this.pod_id = this.podId; - } - } - - public podId!: number; - - @Column({ - name: 'request_id', - type: 'int', - nullable: false, - }) - protected request_id!: number; - - @Column({ - name: 'pod_id', - type: 'int', - nullable: false, - }) - protected pod_id!: number; - - @CreateDateColumn({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @Column({ - name: 'external_id', - type: 'int', - nullable: true, - unsigned: true, - default: 'NULL', - unique: true, - }) - public externalId!: number; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts deleted file mode 100644 index 9bd64266..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - PrimaryGeneratedColumn, - Entity, - CreateDateColumn, - UpdateDateColumn, - ManyToMany, - JoinTable, -} from 'typeorm'; - -import { Pods, IPods } from './index'; - -export type IRequests = Requests; - -@Entity('requests') -export class Requests { - @PrimaryGeneratedColumn() - public id!: number; - - @CreateDateColumn({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToMany(() => Pods, (item) => item.lockedRequests) - @JoinTable({ - name: 'requests_have_pod_locks', - inverseJoinColumn: { - referencedColumnName: 'id', - name: 'pod_id', - }, - joinColumn: { - referencedColumnName: 'id', - name: 'request_id', - }, - }) - public podLocks!: IPods[]; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts deleted file mode 100644 index 4689628a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - PrimaryGeneratedColumn, - Entity, - Column, - ManyToMany, - UpdateDateColumn, -} from 'typeorm'; - -import { Users, IUsers } from './index'; - -@Entity('roles') -export class Roles { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'varchar', - length: 128, - nullable: true, - default: 'NULL', - }) - public name!: string; - - @Column({ - type: 'varchar', - length: 128, - nullable: false, - unique: true, - }) - public key!: string; - - @Column({ - name: 'is_default', - type: 'boolean', - default: 'false', - }) - public isDefault!: boolean; - - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToMany(() => Users, (item) => item.roles) - public users!: IUsers[]; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts deleted file mode 100644 index a6727416..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PrimaryGeneratedColumn, OneToMany, Entity, Column } from 'typeorm'; - -import { IUsers, Users } from './index'; - -@Entity('user_groups') -export class UserGroups { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'varchar', - length: 50, - nullable: false, - unique: true, - }) - public label!: string; - - @OneToMany(() => Users, (item) => item.userGroup) - public users!: IUsers[]; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts deleted file mode 100644 index bdf61878..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { - PrimaryGeneratedColumn, - ManyToMany, - JoinColumn, - JoinTable, - OneToOne, - OneToMany, - Entity, - Column, - UpdateDateColumn, - ManyToOne, -} from 'typeorm'; - -import { Addresses, Roles, Comments, Notes, UserGroups } from './index'; - -export type IUsers = Users; - -@Entity('users') -export class Users { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'varchar', - length: 100, - nullable: false, - unique: true, - }) - public login!: string; - - @Column({ - name: 'first_name', - type: 'varchar', - length: 100, - nullable: true, - default: 'NULL', - }) - public firstName!: string; - - @Column({ - name: 'test_real', - type: 'real', - array: true, - default: [], - }) - public testReal!: number[]; - - @Column({ - name: 'test_array_null', - type: 'real', - array: true, - nullable: true, - }) - public testArrayNull!: number[] | null; - - @Column({ - name: 'last_name', - type: 'varchar', - length: 100, - nullable: true, - default: 'NULL', - }) - public lastName!: string; - - @Column({ - name: 'is_active', - type: 'boolean', - width: 1, - nullable: true, - default: false, - }) - public isActive!: boolean; - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @Column({ - name: 'test_date', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public testDate!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @OneToOne(() => Addresses, (item) => item.id) - @JoinColumn({ - name: 'addresses_id', - }) - public addresses!: Addresses; - - @OneToOne(() => Users, (item) => item.id) - @JoinColumn({ - name: 'manager_id', - }) - public manager!: Users; - - @ManyToMany(() => Roles, (item) => item.users) - @JoinTable({ - name: 'users_have_roles', - inverseJoinColumn: { - referencedColumnName: 'id', - name: 'role_id', - }, - joinColumn: { - referencedColumnName: 'id', - name: 'user_id', - }, - }) - public roles!: Roles[]; - - @OneToMany(() => Comments, (item) => item.createdBy) - public comments!: Comments[]; - - @OneToMany(() => Notes, (item) => item.createdBy) - public notes!: Notes[]; - - @ManyToOne(() => UserGroups, (userGroup) => userGroup.id) - @JoinColumn({ name: 'user_groups_id' }) - public userGroup!: UserGroups; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts deleted file mode 100644 index bd6b43f6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { TypeOrmModule } from '@nestjs/typeorm'; -import { DynamicModule } from '@nestjs/common'; -import { DataType, IMemoryDb, newDb } from 'pg-mem'; -import { readFileSync } from 'fs'; -import { join } from 'path'; - -import { - Users, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - UserGroups, - Notes, -} from './entities'; -import { DataSource } from 'typeorm'; - -import { v4 } from 'uuid'; - -export * from './entities'; -export * from './utils'; - -export const entities = [ - Users, - UserGroups, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - Notes, -]; - -export function createAndPullSchemaBase(): IMemoryDb { - const dump = readFileSync(join(__dirname, 'db-for-test'), { - encoding: 'utf8', - }); - const db = newDb({ - autoCreateForeignKeyIndices: true, - }); - - db.public.registerFunction({ - name: 'current_database', - implementation: () => 'test', - }); - - db.public.registerFunction({ - name: 'version', - implementation: () => - 'PostgreSQL 12.5 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.2.1_pre1) 10.2.1 20201203, 64-bit', - }); - - db.registerExtension('uuid-ossp', (schema) => { - schema.registerFunction({ - name: 'uuid_generate_v4', - returns: DataType.uuid, - implementation: v4, - impure: true, - }); - }); - db.public.none(dump); - return db; -} -export function mockDBTestModule(db: IMemoryDb): DynamicModule { - return TypeOrmModule.forRootAsync({ - useFactory() { - return { - type: 'postgres', - // logging: true, - entities: [ - Users, - UserGroups, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - Notes, - ], - }; - }, - async dataSourceFactory(options) { - const dataSource: DataSource = await db.adapters.createTypeormDataSource( - options - ); - - return dataSource; - }, - }); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts deleted file mode 100644 index 7cb5aa8b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './pull-data'; -export * from './provider-entities'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts deleted file mode 100644 index 4b47577b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Provider } from '@nestjs/common'; -import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm'; - -import { - Addresses, - Comments, - Entities, - Notes, - Pods, - Roles, - UserGroups, -} from '../entities'; -import { Users } from '../entities'; -import { DEFAULT_CONNECTION_NAME } from '../../constants'; -import { TestingModule } from '@nestjs/testing'; - -export function providerEntities( - dataSourceToken: ReturnType -): Provider[] { - return Entities.map((entitiy) => { - return { - provide: getRepositoryToken(entitiy, DEFAULT_CONNECTION_NAME), - useFactory(dataSource: DataSource) { - return dataSource.getRepository(entitiy); - }, - inject: [getDataSourceToken()], - }; - }); -} - -export function getRepository(module: TestingModule) { - const userRepository = module.get>( - getRepositoryToken(Users, DEFAULT_CONNECTION_NAME) - ); - - const addressesRepository = module.get>( - getRepositoryToken(Addresses, DEFAULT_CONNECTION_NAME) - ); - - const notesRepository = module.get>( - getRepositoryToken(Notes, DEFAULT_CONNECTION_NAME) - ); - - const commentsRepository = module.get>( - getRepositoryToken(Comments, DEFAULT_CONNECTION_NAME) - ); - const rolesRepository = module.get>( - getRepositoryToken(Roles, DEFAULT_CONNECTION_NAME) - ); - - const userGroupRepository = module.get>( - getRepositoryToken(UserGroups, DEFAULT_CONNECTION_NAME) - ); - - const podsRepository = module.get>( - getRepositoryToken(Pods, DEFAULT_CONNECTION_NAME) - ); - - return { - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - podsRepository, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts deleted file mode 100644 index f28c693e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Repository } from 'typeorm'; -import { faker } from '@faker-js/faker'; -import { - Addresses, - CommentKind, - Comments, - Notes, - Roles, - UserGroups, - Users, -} from '../entities'; - -export async function pullAddress(addressRepo: Repository) { - const address = new Addresses(); - address.city = faker.location.city(); - address.country = faker.location.country(); - address.arrayField = [ - faker.string.alphanumeric(5), - faker.string.alphanumeric(5), - ]; - address.state = faker.location.state(); - return addressRepo.save(address); -} - -export async function pullComment(commentRepo: Repository) { - const comment = new Comments(); - comment.text = faker.lorem.paragraph(faker.number.int(5)); - comment.kind = CommentKind.Comment; - return commentRepo.save(comment); -} - -export async function pullNote(noteRepo: Repository) { - const note = new Notes(); - note.text = faker.lorem.paragraph(faker.number.int(5)); - return noteRepo.save(note); -} - -export async function pullRole(roleRepo: Repository) { - const role = new Roles(); - role.key = faker.string.alphanumeric(5); - role.name = faker.string.alphanumeric(5); - return roleRepo.save(role); -} - -export async function pullUser(userPero: Repository) { - const user = new Users(); - user.firstName = faker.person.firstName(); - user.lastName = faker.person.lastName(); - user.isActive = faker.datatype.boolean(); - user.login = faker.internet.userName({ - lastName: user.lastName, - firstName: user.firstName, - }); - user.testReal = [faker.number.float({ fractionDigits: 4 })]; - user.testArrayNull = null; - - user.testDate = faker.date.anytime(); - - return userPero.save(user); -} - -export async function pullUserGroup(userGroupRepo: Repository) { - const userGroup = new UserGroups(); - userGroup.label = faker.string.alphanumeric(5); - return userGroupRepo.save(userGroup); -} - -export async function pullAllData( - userPero: Repository, - addressRepo?: Repository, - noteRepo?: Repository, - commentRepo?: Repository, - roleRepo?: Repository, - userGroupRepo?: Repository -) { - const user = await pullUser(userPero); - if (addressRepo) { - user.addresses = await pullAddress(addressRepo); - } - - if (noteRepo) { - user.notes = [ - await pullNote(noteRepo), - await pullNote(noteRepo), - await pullNote(noteRepo), - ]; - } - - if (commentRepo) { - user.comments = [ - await pullComment(commentRepo), - await pullComment(commentRepo), - await pullComment(commentRepo), - await pullComment(commentRepo), - ]; - } - - if (userGroupRepo) { - await pullUserGroup(userGroupRepo); - await pullUserGroup(userGroupRepo); - await pullUserGroup(userGroupRepo); - user.userGroup = await pullUserGroup(userGroupRepo); - } - - if (roleRepo) { - await pullRole(roleRepo); - await pullRole(roleRepo); - await pullRole(roleRepo); - user.roles = [ - await pullRole(roleRepo), - await pullRole(roleRepo), - await pullRole(roleRepo), - ]; - } - - user.manager = await pullUser(userPero); - await pullUser(userPero); - await pullUser(userPero); - await pullUser(userPero); - await userPero.save(user); - return user; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts deleted file mode 100644 index ec16128c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { AsyncLocalStorage } from 'async_hooks'; -import { - DynamicModule, - Inject, - MiddlewareConsumer, - Module, - NestModule, -} from '@nestjs/common'; -import { DiscoveryModule } from '@nestjs/core'; - -import { OperationController } from './controllers'; -import { ExplorerService, ExecuteService, SwaggerService } from './service'; - -import { - MapControllerEntity, - MapEntityNameToEntity, - ZodInputOperation, - AsyncIterate, -} from './factory'; -import { ModuleOptions } from '../../types'; -import { MAP_CONTROLLER_INTERCEPTORS, OPTIONS } from './constants'; - -@Module({}) -export class AtomicOperationModule implements NestModule { - static forRoot( - options: ModuleOptions, - entityModules: DynamicModule[], - commonModule: DynamicModule - ): DynamicModule { - return { - module: AtomicOperationModule, - controllers: [OperationController], - providers: [ - ExplorerService, - ExecuteService, - SwaggerService, - AsyncIterate, - MapControllerEntity(options.entities, entityModules), - MapEntityNameToEntity(options.entities), - ZodInputOperation(options.connectionName), - { - provide: MAP_CONTROLLER_INTERCEPTORS, - useValue: new Map(), - }, - { - provide: OPTIONS, - useValue: options.options, - }, - { - provide: AsyncLocalStorage, - useValue: new AsyncLocalStorage(), - }, - ], - imports: [DiscoveryModule, commonModule], - }; - } - @Inject(AsyncLocalStorage) private readonly als!: AsyncLocalStorage; - - configure(consumer: MiddlewareConsumer) { - consumer - .apply((req: any, res: any, next: any) => { - const store = { - req: req, - res: res, - next: next, - }; - this.als.run(store, () => next()); - }) - .forRoutes('*'); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts deleted file mode 100644 index ccace714..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const MAP_CONTROLLER_ENTITY = Symbol('MAP_CONTROLLER_ENTITY'); -export const MAP_CONTROLLER_INTERCEPTORS = Symbol( - 'MAP_CONTROLLER_INTERCEPTORS' -); -export const MAP_ENTITY = Symbol('MAP_ENTITY'); -export const ZOD_INPUT_OPERATION = Symbol('ZOD_INPUT_OPERATION'); -export const ASYNC_ITERATOR_FACTORY = Symbol('ASYNC_ITERATOR_FACTORY'); -export const KEY_MAIN_INPUT_SCHEMA = 'atomic:operations'; -export const KEY_MAIN_OUTPUT_SCHEMA = 'atomic:results'; -export const OPTIONS = Symbol('OPTIONS'); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts deleted file mode 100644 index e81188ae..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './operation.controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts deleted file mode 100644 index af5511e2..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { DiscoveryModule } from '@nestjs/core'; -import { HttpException } from '@nestjs/common'; -import { Module } from '@nestjs/core/injector/module'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { OperationController } from './operation.controller'; -import { ExecuteService, ExplorerService } from '../service'; -import { InputArray, Operation } from '../utils'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { - ASYNC_ITERATOR_FACTORY, - KEY_MAIN_OUTPUT_SCHEMA, - MAP_CONTROLLER_ENTITY, - MAP_ENTITY, - ZOD_INPUT_OPERATION, - MAP_CONTROLLER_INTERCEPTORS, - OPTIONS, -} from '../constants'; - -import { CurrentDataSourceProvider } from '../../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../constants'; -import { OperationMethode } from '../types'; -import { AsyncLocalStorage } from 'async_hooks'; - -describe('OperationController', () => { - let db: IMemoryDb; - let operationController: OperationController; - let explorerService: ExplorerService; - let executeService: ExecuteService; - - beforeEach(async () => { - db = createAndPullSchemaBase(); - const app: TestingModule = await Test.createTestingModule({ - imports: [DiscoveryModule, mockDBTestModule(db)], - controllers: [OperationController], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - ExplorerService, - ExecuteService, - { - provide: MAP_ENTITY, - useValue: {}, - }, - { - provide: MAP_CONTROLLER_ENTITY, - useValue: {}, - }, - { - provide: ASYNC_ITERATOR_FACTORY, - useValue: {}, - }, - { - provide: ZOD_INPUT_OPERATION, - useValue: {}, - }, - { - provide: OPTIONS, - useValue: {}, - }, - { - provide: MAP_CONTROLLER_INTERCEPTORS, - useValue: {}, - }, - { - provide: AsyncLocalStorage, - useValue: new AsyncLocalStorage(), - }, - ], - }).compile(); - - operationController = app.get(OperationController); - explorerService = app.get>(ExplorerService); - executeService = app.get(ExecuteService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - describe('index', () => { - it('should return the result of executeService.run', async () => { - const inputArrayMock: InputArray = [ - { - ref: { - id: '1', - relationship: 'belongs-to', - type: 'TypeA', - }, - op: Operation.add, - }, - ]; - const paramsForExecuteMock = [ - { - module: new (class Module {})() as Module, - params: [1, 'nameRel', { type: 'name', id: '' }], - methodName: 'patchOne', - controller: JsonBaseController, - }, - ]; - - const mockReturnData = { data: { someData: '' } }; - - const getControllerByEntityNameSpy = jest - .spyOn(explorerService, 'getControllerByEntityName') - .mockReturnValue(paramsForExecuteMock[0].controller); - const getMethodNameByParamSpy = jest - .spyOn(explorerService, 'getMethodNameByParam') - .mockReturnValue( - paramsForExecuteMock[0].methodName as OperationMethode - ); - const getModulesByControllerSpy = jest - .spyOn(explorerService, 'getParamsForMethod') - .mockReturnValue( - paramsForExecuteMock[0].params as Parameters< - JsonBaseController['deleteOne'] - > - ); - const getParamsForMethodSpy = jest - .spyOn(explorerService, 'getModulesByController') - .mockReturnValue(paramsForExecuteMock[0].module); - const runSpy = jest - .spyOn(executeService, 'run') - .mockResolvedValue([mockReturnData] as never); - - expect(await operationController.index(inputArrayMock)).toEqual({ - [KEY_MAIN_OUTPUT_SCHEMA]: [mockReturnData], - }); - - expect(getControllerByEntityNameSpy).toHaveBeenCalledWith('TypeA'); - expect(getMethodNameByParamSpy).toHaveBeenCalledWith( - inputArrayMock[0].op, - inputArrayMock[0].ref.id, - inputArrayMock[0].ref.relationship - ); - expect(getModulesByControllerSpy).toHaveBeenCalledWith( - paramsForExecuteMock[0].methodName, - { op: inputArrayMock[0].op, ref: inputArrayMock[0].ref } - ); - expect(getParamsForMethodSpy).toHaveBeenCalledWith( - paramsForExecuteMock[0].controller - ); - // expect(runSpy).toHaveBeenCalledWith(paramsForExecuteMock[0].module, paramsForExecuteMock[0].methodName, paramsForExecuteMock[0].params); - - expect(runSpy).toHaveBeenCalledWith(paramsForExecuteMock, []); - }); - - it('should throw NotFoundException when type does not exist', async () => { - const inputArrayMock: any[] = [ - { - ref: { - id: '1', - relationship: 'belongs-to', - type: 'TypeA', - }, - op: Operation.add, - }, - ]; - - jest - .spyOn(explorerService, 'getControllerByEntityName') - .mockImplementationOnce(() => { - throw new HttpException('Resource does not exist', 404); - }); - expect.assertions(1); - try { - await operationController.index(inputArrayMock as InputArray); - } catch (e) { - expect(e).toBeInstanceOf(HttpException); - } - }); - - it('should throw MethodNotAllowedException when operation not allowed', async () => { - const inputArrayMock = [ - { - ref: { - id: '1', - relationship: 'belongs-to', - type: 'TypeA', - }, - op: Operation.add, - }, - ]; - - jest - .spyOn(explorerService, 'getControllerByEntityName') - .mockReturnValue(Promise.resolve({}) as any); - - jest - .spyOn(explorerService, 'getMethodNameByParam') - .mockImplementationOnce(() => { - throw new HttpException('Operation not allowed', 405); - }); - - expect.assertions(1); - try { - await operationController.index(inputArrayMock as InputArray); - } catch (e) { - expect(e).toBeInstanceOf(HttpException); - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts deleted file mode 100644 index 4ce1510b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - Body, - Controller, - Inject, - MethodNotAllowedException, - NotFoundException, - Post, - Type, -} from '@nestjs/common'; -import { Module } from '@nestjs/core/injector/module'; - -import { InputArray } from '../utils'; -import { InputOperationPipe } from '../pipes/input-operation.pipe'; -import { ExecuteService, ExplorerService } from '../service'; -import { KEY_MAIN_INPUT_SCHEMA, KEY_MAIN_OUTPUT_SCHEMA } from '../constants'; -import { OperationMethode, ParamsForExecute } from '../types'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; -import { Entity, ValidateQueryError } from '../../../types'; - -@Controller('/') -export class OperationController { - @Inject(ExplorerService) private readonly explorerService!: ExplorerService; - @Inject(ExecuteService) private readonly executeService!: ExecuteService; - - @Post('') - async index(@Body(InputOperationPipe) inputOperationData: InputArray) { - const paramForCall: ParamsForExecute[] = []; - let i = 0; - for (const dataInput of inputOperationData) { - const { - ref: { relationship, id, type }, - op, - } = dataInput; - - let controller: Type>; - let methodName: OperationMethode; - let module: Module; - try { - controller = this.explorerService.getControllerByEntityName(type); - } catch (e) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${type}' does not exist`, - path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'ref', 'type'], - }; - throw new NotFoundException([error]); - } - try { - methodName = this.explorerService.getMethodNameByParam( - op, - id, - relationship - ); - } catch (e) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Operation '${op}' not allowed`, - path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'op'], - }; - throw new MethodNotAllowedException([error]); - } - - const params = this.explorerService.getParamsForMethod( - methodName, - dataInput - ); - - try { - module = this.explorerService.getModulesByController(controller); - } catch (e) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${type}' does not exist`, - path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'ref', 'type'], - }; - throw new NotFoundException([error]); - } - - paramForCall.push({ - controller, - methodName, - params, - module, - }); - - i++; - } - const tmpIds: (string | number)[] = []; - for (const item of inputOperationData) { - if (item.op !== 'add') continue; - if (!item.ref.tmpId) continue; - tmpIds.push(item.ref.tmpId); - } - - const result = await this.executeService.run(paramForCall, tmpIds); - - return { - [KEY_MAIN_OUTPUT_SCHEMA]: result.map((i) => ({ - data: i.data, - ...(i.meta ? { meta: i.meta } : {}), - })), - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts deleted file mode 100644 index fd9f15b1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Provider } from '@nestjs/common'; -import { ASYNC_ITERATOR_FACTORY } from '../constants'; - -type ParamsInput = R extends (...arg: infer P) => any ? P : never; - -type ParamsReturn = R extends (...arg: any) => infer P - ? P extends Promise - ? T extends [infer K, ...any] - ? K - : T - : P - : never; - -export type IterateFactory< - R extends (...arg: any) => any = (...arg: any) => any -> = { - createIterator: ( - iterateObject: ParamsInput, - callback: R - ) => { - [Symbol.asyncIterator](): GeneralAsyncIterator< - R, - ParamsInput, - ParamsReturn - >; - }; -}; - -class GeneralAsyncIterator< - R extends (...arg: any[]) => any, - T = ParamsInput, - TReturn = ParamsReturn -> implements AsyncIterator -{ - private counter = 0; - private maxLimit!: number; - - constructor(private iterateObject: T[], private callback: R) { - if (!Array.isArray(iterateObject)) { - throw new Error('Expected iterateObject to be an array'); - } - this.maxLimit = iterateObject.length; - } - - async next(): Promise> { - const items = !Array.isArray(this.iterateObject[this.counter]) - ? [this.iterateObject[this.counter]] - : (this.iterateObject[this.counter] as T[]); - this.counter++; - - if (this.counter <= this.maxLimit) { - return this.callback(...items).then((r: TReturn) => ({ - done: false, - value: r, - })); - } else { - return Promise.resolve({ done: true, value: {} as TReturn }); - } - } -} - -export const AsyncIterate: Provider = { - provide: ASYNC_ITERATOR_FACTORY, - useFactory: () => ({ - createIterator any>( - iterateObject: ParamsInput, - callback: R - ) { - return { - [Symbol.asyncIterator]: () => - new GeneralAsyncIterator(iterateObject, callback), - }; - }, - }), -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts deleted file mode 100644 index 0dded8b9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './zod-input-operation'; -export * from './map-controller-entity'; -export * from './map-entity-name-to-entity'; -export * from './async-iterator'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts deleted file mode 100644 index 64d23a17..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DynamicModule, ValueProvider } from '@nestjs/common'; -import { Type } from '@nestjs/common/interfaces/type.interface'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { MapController } from '../types'; -import { MAP_CONTROLLER_ENTITY } from '../constants'; - -export function MapControllerEntity( - entities: EntityClassOrSchema[], - entityModules: DynamicModule[] -): ValueProvider { - const mapController = entities.reduce((acum, entity, index) => { - const entityModule = entityModules[index]; - if (entityModule.controllers) { - const controller = entityModule.controllers.at(0); - if (controller) acum.set(entity, controller); - } - - return acum; - }, new Map>()); - - return { - provide: MAP_CONTROLLER_ENTITY, - useValue: mapController, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts deleted file mode 100644 index 8149f47a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { ValueProvider } from '@nestjs/common'; -import { MapEntity } from '../types'; -import { MAP_ENTITY } from '../constants'; -import { camelToKebab, getEntityName } from '../../../helper'; - -export function MapEntityNameToEntity( - entities: EntityClassOrSchema[] -): ValueProvider { - return { - provide: MAP_ENTITY, - useValue: entities.reduce( - (acum, item) => acum.set(camelToKebab(getEntityName(item)), item), - new Map() - ), - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts deleted file mode 100644 index 21d58521..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DataSource } from 'typeorm'; -import { FactoryProvider } from '@nestjs/common'; -import { MAP_CONTROLLER_ENTITY, ZOD_INPUT_OPERATION } from '../constants'; -import { MapController } from '../types'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; -import { - zodInputOperation, - ZodInputOperation as TypeZodInputOperation, -} from '../utils/zod/zod-helper'; - -export function ZodInputOperation( - connectionName?: string -): FactoryProvider { - return { - provide: ZOD_INPUT_OPERATION, - useFactory(dataSource: DataSource, mapController: MapController) { - return zodInputOperation(dataSource, mapController); - }, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - { - token: MAP_CONTROLLER_ENTITY, - optional: false, - }, - ], - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts deleted file mode 100644 index 643405e1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ZodError } from 'zod'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; - -import { InputOperationPipe } from './input-operation.pipe'; - -import { KEY_MAIN_INPUT_SCHEMA, ZOD_INPUT_OPERATION } from '../constants'; -import { ZodInputOperation } from '../utils'; - -describe('PatchInputPipe', () => { - let patchInputPipe: InputOperationPipe; - let zodInputOperation: ZodInputOperation; - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: ZOD_INPUT_OPERATION, - useValue: { - parse() {}, - }, - }, - InputOperationPipe, - ], - }).compile(); - - patchInputPipe = module.get(InputOperationPipe); - zodInputOperation = module.get(ZOD_INPUT_OPERATION); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - [KEY_MAIN_INPUT_SCHEMA]: data, - }; - jest - .spyOn(zodInputOperation, 'parse') - .mockImplementationOnce(() => check as any); - expect(patchInputPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest.spyOn(zodInputOperation, 'parse').mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodInputOperation, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts deleted file mode 100644 index 11ca8652..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { errorMap } from 'zod-validation-error'; -import { ZodError } from 'zod'; -import { JSONValue } from '../../../types'; -import { InputArray, ZodInputOperation } from '../utils'; -import { KEY_MAIN_INPUT_SCHEMA, ZOD_INPUT_OPERATION } from '../constants'; - -export class InputOperationPipe - implements PipeTransform -{ - @Inject(ZOD_INPUT_OPERATION) private zodInputOperation!: ZodInputOperation; - - transform(value: JSONValue): InputArray { - try { - return this.zodInputOperation.parse(value, { - errorMap: errorMap, - })[KEY_MAIN_INPUT_SCHEMA]; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts deleted file mode 100644 index 5b2f6bb7..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts +++ /dev/null @@ -1,478 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ModuleRef } from '@nestjs/core'; -import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; -import { DataSource } from 'typeorm'; -import { ExecuteService, isZodError } from './execute.service'; -import { IterateFactory } from '../factory'; -import { - ASYNC_ITERATOR_FACTORY, - KEY_MAIN_INPUT_SCHEMA, - MAP_CONTROLLER_INTERCEPTORS, - OPTIONS, -} from '../constants'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; -import { - HttpException, - NotFoundException, - ParseIntPipe, - ParseBoolPipe, -} from '@nestjs/common'; -import { ParamsForExecute } from '../types'; -import { AsyncLocalStorage } from 'async_hooks'; - -describe('ExecuteService', () => { - let service: ExecuteService; - let dataSource: DataSource; - let moduleRef: ModuleRef; - let asyncIteratorFactory: IterateFactory; - let mapControllerInterceptors = new Map(); - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ExecuteService, - { - provide: CURRENT_DATA_SOURCE_TOKEN, - useValue: { - createQueryRunner: () => {}, - }, - }, - { - provide: ModuleRef, - useValue: { - get() {}, - }, - }, - { - provide: OPTIONS, - useValue: {}, - }, - { - provide: ASYNC_ITERATOR_FACTORY, - useValue: { - createIterator: () => {}, - }, - }, - { - provide: MAP_CONTROLLER_INTERCEPTORS, - useValue: mapControllerInterceptors, - }, - { - provide: AsyncLocalStorage, - useValue: new AsyncLocalStorage(), - }, - ], - }).compile(); - - service = module.get(ExecuteService); - dataSource = module.get(CURRENT_DATA_SOURCE_TOKEN); - moduleRef = module.get(ModuleRef); - asyncIteratorFactory = module.get(ASYNC_ITERATOR_FACTORY); - mapControllerInterceptors.clear(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('run', () => { - it('should throw NotFoundException if controller not found', async () => { - const params = [ - { - controller: { name: 'NonExistentController' }, - module: { controllers: new Map() }, - }, - ] as ParamsForExecute[]; - - const queryRunnerMock = { - startTransaction: jest.fn(), - commitTransaction: jest.fn(), - rollbackTransaction: jest.fn(), - release: jest.fn(), - }; - jest - .spyOn(dataSource, 'createQueryRunner') - .mockReturnValue(queryRunnerMock as any); - - jest.spyOn(service as any, 'executeOperations').mockImplementation(() => { - throw new NotFoundException(); - }); - - await expect(service.run(params, [])).rejects.toThrow(NotFoundException); - - expect(queryRunnerMock.rollbackTransaction).toHaveBeenCalled(); - expect(queryRunnerMock.release).toHaveBeenCalled(); - await expect(service.run(params, [])).rejects.toThrow(NotFoundException); - }); - - it('should return an empty array if no operations are executed', async () => { - const params: ParamsForExecute[] = []; - - const queryRunnerMock = { - startTransaction: jest.fn(), - commitTransaction: jest.fn(), - rollbackTransaction: jest.fn(), - release: jest.fn(), - }; - jest - .spyOn(dataSource, 'createQueryRunner') - .mockReturnValue(queryRunnerMock as any); - - jest.spyOn(service as any, 'executeOperations').mockReturnValue([]); - - const result = await service.run(params, []); - expect(result).toEqual([]); - - expect(queryRunnerMock.startTransaction).toHaveBeenCalled(); - expect(queryRunnerMock.commitTransaction).toHaveBeenCalled(); - expect(queryRunnerMock.rollbackTransaction).not.toHaveBeenCalled(); - expect(queryRunnerMock.release).toHaveBeenCalled(); - }); - }); - - describe('executeOperations', () => { - it('should correctly execute operations', async () => { - const params: ParamsForExecute[] = [ - { - controller: { name: 'TestController' }, - methodName: 'someMethod', - }, - ] as unknown as ParamsForExecute[]; - const callback = jest.fn().mockReturnValue({ value: 'test' }); - const mapController = { - someMethod: callback, - }; - jest - .spyOn(service as any, 'getControllerInstance') - .mockReturnValue(mapController); - - mapControllerInterceptors.set(mapController, new Map([[callback, []]])); - let callCount = 0; - jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ - [Symbol.asyncIterator]: () => - ({ - next: () => { - callCount++; - if (callCount === 1) { - return Promise.resolve({ value: 'test', done: false }); - } else { - return Promise.resolve({ value: undefined, done: true }); - } - }, - } as any), - }); - - const result = await (service as any).executeOperations(params, []); - - expect(result).toEqual([{ value: 'test' }]); - }); - - it('should return an empty array if controller method does not return an object', async () => { - const params: ParamsForExecute[] = [ - { - controller: { name: 'TestController' }, - methodName: 'someMethod', - }, - ] as unknown as ParamsForExecute[]; - - const callback = jest.fn().mockReturnValue('not an object'); - const mapController = { - someMethod: callback, - }; - jest - .spyOn(service as any, 'getControllerInstance') - .mockReturnValue(mapController); - - mapControllerInterceptors.set(mapController, new Map([[callback, []]])); - - let callCount = 0; - jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ - [Symbol.asyncIterator]: () => - ({ - next: () => { - callCount++; - if (callCount === 1) { - return Promise.resolve({ value: 'not an object', done: false }); - } else { - return Promise.resolve({ value: undefined, done: true }); - } - }, - } as any), - }); - - const result = await (service as any).executeOperations(params, []); - - expect(result).toEqual([]); - }); - - it('should call processException if an exception is thrown during execution', async () => { - const params: ParamsForExecute[] = [ - { - controller: { name: 'TestController' }, - methodName: 'someMethod', - }, - ] as unknown as ParamsForExecute[]; - - const callback = jest.fn().mockImplementation(() => { - throw new HttpException('Test exception', 400); - }); - const mapController = { - someMethod: callback, - }; - jest - .spyOn(service as any, 'getControllerInstance') - .mockReturnValue(mapController); - - mapControllerInterceptors.set(mapController, new Map([[callback, []]])); - - let callCount = 0; - jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ - [Symbol.asyncIterator]: () => - ({ - next: () => { - callCount++; - if (callCount === 1) { - return Promise.resolve({ value: 'test', done: false }); - } else { - return Promise.resolve({ value: undefined, done: true }); - } - }, - } as any), - }); - - const processExceptionSpy = jest.spyOn( - service as any, - 'processException' - ); - - await expect((service as any).executeOperations(params)).rejects.toThrow( - HttpException - ); - - expect(processExceptionSpy).toHaveBeenCalled(); - }); - }); - - describe('getControllerInstance', () => { - it('should throw NotFoundException if controller not found', () => { - const params: ParamsForExecute = { - controller: { name: 'NonExistentController' }, - module: { controllers: new Map() }, - } as unknown as ParamsForExecute; - - expect(() => (service as any).getControllerInstance(params)).toThrow( - NotFoundException - ); - }); - - it('should return controller instance if controller is found', () => { - const controllerInstance = { - someMethod: jest.fn().mockReturnValue('test'), - }; - function TestController() {} - const params: ParamsForExecute = { - controller: TestController, - methodName: 'someMethod', - module: { - controllers: new Map([ - [TestController, { instance: controllerInstance }], - ]), - }, - } as unknown as ParamsForExecute; - - const result = (service as any).getControllerInstance(params); - - expect(result).toBe(controllerInstance); - }); - }); - - describe('processException', () => { - it('should rethrow HttpException with modified response if ZodError is thrown', () => { - const exception = new HttpException( - { - message: [{ path: ['test'] }], - }, - 400 - ); - - try { - (service as any).processException(exception, 1); - } catch (e) { - if (e instanceof HttpException) { - const response = e.getResponse(); - if (isZodError(response)) { - expect(response['message'][0]['path']).toEqual([ - KEY_MAIN_INPUT_SCHEMA, - '1', - 'test', - ]); - } else { - fail('Exception response is not a ZodError'); - } - } else { - fail('Caught exception is not a HttpException'); - } - } - }); - - it('should rethrow the original exception if it is not a HttpException', () => { - const exception = new Error('Test exception'); - - expect(() => (service as any).processException(exception, 1)).toThrow( - Error - ); - }); - }); - - describe('runOneOperation', () => { - it('should correctly run operation', async () => { - const controllerInstance = { - someMethod: jest.fn().mockReturnValue('test'), - }; - function TestController() {} - const pipes = [ - { index: 0, pipes: [] }, - { index: 1, pipes: [] }, - ]; - const params: ParamsForExecute = { - controller: TestController, - methodName: 'someMethod', - module: { - controllers: new Map([ - [TestController, { instance: controllerInstance }], - ]), - }, - params: ['param1', 'param2'], - } as unknown as ParamsForExecute; - - Reflect.defineMetadata( - ROUTE_ARGS_METADATA, - { 0: pipes[0], 1: pipes[1] }, - TestController, - 'someMethod' - ); - - const runPipesSpy = jest - .spyOn(service as any, 'runPipes') - .mockImplementation((param) => `modified_${param}`); - - await (service as any).runOneOperation(params); - - expect(runPipesSpy).toHaveBeenCalledWith( - 'param1', - params.module, - pipes[0].pipes - ); - expect(runPipesSpy).toHaveBeenCalledWith( - 'param2', - params.module, - pipes[1].pipes - ); - }); - - it('should not call runPipes if metadata is empty', async () => { - const controllerInstance = { - someMethod: jest.fn().mockReturnValue('test'), - }; - function TestController() {} - const params: ParamsForExecute = { - controller: TestController, - methodName: 'someMethod', - module: { - controllers: new Map([ - [TestController, { instance: controllerInstance }], - ]), - }, - params: ['param1', 'param2'], - } as unknown as ParamsForExecute; - - Reflect.defineMetadata( - ROUTE_ARGS_METADATA, - {}, - TestController, - 'someMethod' - ); - - const runPipesSpy = jest - .spyOn(service as any, 'runPipes') - .mockImplementation((param) => `modified_${param}`); - - await (service as any).runOneOperation(params); - - expect(runPipesSpy).not.toHaveBeenCalled(); - }); - }); - - describe('runPipes', () => { - it('should correctly run pipes', async () => { - const value = 'test'; - const pipes = [new ParseBoolPipe(), new ParseIntPipe()]; - const module = {} as any; - - jest - .spyOn(pipes[0], 'transform') - // @ts-ignore - .mockImplementation((val) => `validated_${val}`); - - jest - .spyOn(pipes[1], 'transform') - // @ts-ignore - .mockImplementation((val) => `parsed_${val}`); - const getPipeInstanceSpy = jest - .spyOn(service as any, 'getPipeInstance') - .mockImplementation((pipe) => - pipe instanceof ParseBoolPipe ? pipes[0] : pipes[1] - ); - - const result = await (service as any).runPipes(value, module, [ - pipes[0], - pipes[1], - ]); - - expect(result).toBe('parsed_validated_test'); - expect(getPipeInstanceSpy).toHaveBeenCalledTimes(2); - expect(getPipeInstanceSpy).toHaveBeenNthCalledWith(1, pipes[0], module); - expect(getPipeInstanceSpy).toHaveBeenNthCalledWith(2, pipes[1], module); - }); - - it('should not call getPipeInstance if pipes array is empty', async () => { - const value = 'test'; - const module = {} as any; - - const getPipeInstanceSpy = jest.spyOn(service as any, 'getPipeInstance'); - - const result = await (service as any).runPipes(value, module, []); - - expect(result).toBe('test'); - expect(getPipeInstanceSpy).not.toHaveBeenCalled(); - }); - }); - - describe('getPipeInstance', () => { - it('should return pipe instance from module if it exists', () => { - const pipe = new ParseBoolPipe(); - const module = { - getProviderByKey: jest.fn().mockReturnValue({ instance: pipe }), - } as any; - - const result = (service as any).getPipeInstance(ParseBoolPipe, module); - - expect(result).toBe(pipe); - expect(module.getProviderByKey).toHaveBeenCalledWith(ParseBoolPipe); - }); - - it('should return pipe instance from moduleRef if it does not exist in module', () => { - const pipe = new ParseBoolPipe(); - const module = { - getProviderByKey: jest.fn().mockReturnValue(null), - } as any; - jest.spyOn(service['moduleRef'], 'get').mockReturnValue(pipe); - - const result = (service as any).getPipeInstance(ParseBoolPipe, module); - - expect(result).toBe(pipe); - expect(service['moduleRef'].get).toHaveBeenCalledWith(ParseBoolPipe, { - strict: false, - }); - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts deleted file mode 100644 index 8bec403f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { - HttpException, - NotFoundException, - Inject, - Injectable, - PipeTransform, - Type, -} from '@nestjs/common'; -import { - INTERCEPTORS_METADATA, - ROUTE_ARGS_METADATA, -} from '@nestjs/common/constants'; -import { Module } from '@nestjs/core/injector/module'; -import { ArgumentMetadata } from '@nestjs/common/interfaces/features/pipe-transform.interface'; -import { ApplicationConfig, ModuleRef, NestContainer } from '@nestjs/core'; -import { DataSource } from 'typeorm'; - -import { MapControllerInterceptor, ParamsForExecute } from '../types'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; -import { - ASYNC_ITERATOR_FACTORY, - KEY_MAIN_INPUT_SCHEMA, - MAP_CONTROLLER_INTERCEPTORS, - OPTIONS, -} from '../constants'; -import { IterateFactory } from '../factory'; -import { - ConfigParam, - ResourceObject, - ResourceObjectRelationships, - TypeFromType, - ValidateQueryError, -} from '../../../types'; -import { ObjectTyped } from '../../../helper'; -import { - InterceptorsConsumer, - InterceptorsContextCreator, -} from '@nestjs/core/interceptors'; -import { Controller } from '@nestjs/common/interfaces'; -import { lastValueFrom } from 'rxjs'; -import { AsyncLocalStorage } from 'async_hooks'; - -export function isZodError( - param: string | unknown -): param is { message: ValidateQueryError[] } { - return ( - param instanceof Object && - 'message' in param && - Array.isArray(param.message) && - 'path' in param.message[0] - ); -} - -@Injectable() -export class ExecuteService { - @Inject(CURRENT_DATA_SOURCE_TOKEN) private readonly dataSource!: DataSource; - @Inject(ModuleRef) private readonly moduleRef!: ModuleRef & { - container: NestContainer; - applicationConfig: ApplicationConfig; - _moduleKey: string; - }; - @Inject(ASYNC_ITERATOR_FACTORY) private asyncIteratorFactory!: IterateFactory< - ExecuteService['runOneOperation'] - >; - @Inject(OPTIONS) private options!: ConfigParam; - @Inject(MAP_CONTROLLER_INTERCEPTORS) - private mapControllerInterceptor!: MapControllerInterceptor; - - @Inject(AsyncLocalStorage) private asyncLocalStorage!: AsyncLocalStorage; - - private _interceptorsContextCreator!: InterceptorsContextCreator; - - get interceptorsContextCreator() { - if (!this._interceptorsContextCreator) { - this._interceptorsContextCreator = new InterceptorsContextCreator( - this.moduleRef.container, - this.moduleRef.applicationConfig - ); - } - - return this._interceptorsContextCreator; - } - - private interceptorsConsumer = new InterceptorsConsumer(); - - async run(params: ParamsForExecute[], tmpIds: (string | number)[]) { - if ( - this.options.runInTransaction && - typeof this.options.runInTransaction === 'function' - ) { - return this.options.runInTransaction('READ UNCOMMITTED', () => { - return this.executeOperations(params, tmpIds); - }); - } - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.startTransaction('READ UNCOMMITTED'); - try { - const resultArray = await this.executeOperations(params, tmpIds); - await queryRunner.commitTransaction(); - return resultArray; - } catch (e) { - await queryRunner.rollbackTransaction(); - throw e; - } finally { - await queryRunner.release(); - } - - return []; - } - - private async executeOperations( - params: ParamsForExecute[], - tmpIds: (string | number)[] = [] - ) { - const iterateParams = this.asyncIteratorFactory.createIterator( - params as Parameters, - this.runOneOperation.bind(this) as ExecuteService['runOneOperation'] - ); - - const resultArray: Array< - ResourceObject | ResourceObjectRelationships - > = []; - let i = 0; - const tmpIdsMap: Record = {}; - try { - for await (const item of iterateParams) { - const currentParams = params[i]; - const controller = this.getControllerInstance(currentParams); - const methodName = - currentParams.methodName as (typeof currentParams)['methodName']; - - const paramsForExecute = item as unknown as ParamsForExecute['params']; - - const itemReplace = this.replaceTmpIds(paramsForExecute, tmpIdsMap); - const body = itemReplace.at(-1); - const currentTmpId = tmpIds[i]; - - if (methodName === 'postOne' && currentTmpId && body) { - if (typeof body === 'object' && 'attributes' in body) { - body['id'] = `${currentTmpId}`; - itemReplace[itemReplace.length - 1]; - } - } - - const interceptors = this.getInterceptorsArray( - controller, - controller[methodName], - currentParams.module - ); - - const result$: any = await this.interceptorsConsumer.intercept( - interceptors, - [ - ...Object.values(this.asyncLocalStorage.getStore() || {}), - itemReplace, - ], - controller, - // @ts-ignore - controller[methodName], - // @ts-ignore - async () => controller[methodName](...itemReplace) - ); - - const result = - interceptors.length === 0 - ? await result$ - : await lastValueFrom(result$); - - if (tmpIds[i] && result && !Array.isArray(result.data) && result.data) { - tmpIdsMap[tmpIds[i]] = result.data.id; - } - - if (result instanceof Object) { - resultArray.push(result); - } - i++; - } - } catch (e) { - this.processException(e, i); - } - return resultArray; - } - - private getInterceptorsArray( - controller: Controller, - callback: (...arg: any) => any, - module: ParamsForExecute['module'] - ) { - let controllerFromMap = this.mapControllerInterceptor.get(controller); - - if (!controllerFromMap) { - controllerFromMap = new Map(); - this.mapControllerInterceptor.set(controller, controllerFromMap); - } - - const interceptorsFromMap = controllerFromMap.get(callback); - - if (interceptorsFromMap) { - return interceptorsFromMap; - } - - const interceptorsForController = this.interceptorsContextCreator.create( - controller, - callback, - module.token - ); - - const interceptorsForMethode = new Set( - Reflect.getMetadata(INTERCEPTORS_METADATA, callback) || [] - ); - - const resultInterceptors = interceptorsForController.filter((i) => - interceptorsForMethode.has(i.constructor) - ); - controllerFromMap.set(callback, resultInterceptors); - return resultInterceptors; - } - - private replaceTmpIds( - inputParams: T, - tmpIdsMap: Record - ): T { - const bodyInput = inputParams.at(-1); - if (!bodyInput) { - return inputParams; - } - if (typeof bodyInput === 'string') { - return inputParams; - } - if (typeof bodyInput === 'number') { - return inputParams; - } - - if (Array.isArray(bodyInput)) { - return inputParams; - } - - if (!('relationships' in bodyInput)) { - return inputParams; - } - - const { relationships } = bodyInput; - if (!relationships) { - return inputParams; - } - - bodyInput.relationships = ObjectTyped.entries(relationships).reduce( - (acum, [name, val]) => { - if (Array.isArray(val)) { - acum[name] = (val as any[]).map((i) => { - i['id'] = tmpIdsMap[i['id']] ? tmpIdsMap[i['id']] : i['id']; - return i; - }) as never; - } else { - acum[name]['id'] = tmpIdsMap[val['id']] - ? (tmpIdsMap[val['id']] as never) - : acum[name]['id']; - } - return acum; - }, - // @ts-ignore - { ...relationships } - ); - - inputParams[inputParams.length - 1] = bodyInput; - return inputParams; - } - - private getControllerInstance(params: ParamsForExecute) { - const controllerClass = params.controller; - const controllerInstanceWrapper = - params.module.controllers.get(controllerClass); - - if (!controllerInstanceWrapper) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - path: ['type'], - message: `Controller "${controllerClass.name}" not found`, - }; - throw new NotFoundException([error]); - } - - return controllerInstanceWrapper.instance as TypeFromType< - ParamsForExecute['controller'] - >; - } - - private processException(e: any, i: number) { - if (e instanceof HttpException) { - const response = e.getResponse(); - if (isZodError(response)) { - response['message'] = response['message'].map((m: any) => { - m['path'] = [KEY_MAIN_INPUT_SCHEMA, `${i}`, ...m['path']]; - return m; - }); - } - throw new HttpException(response, e.getStatus()); - } - throw e; - } - - private async runOneOperation( - paramForExecute: ParamsForExecute - ): Promise { - const { params, controller, methodName, module } = paramForExecute; - const pramsPipe = Object.values( - Reflect.getMetadata(ROUTE_ARGS_METADATA, controller, methodName) - ) as unknown as { - index: number; - pipes: Type[]; - }[]; - const resultParams = new Array(params.length); - for (const { pipes, index } of pramsPipe) { - resultParams[index] = await this.runPipes(params[index], module, pipes); - } - return resultParams as unknown as ParamsForExecute['params']; - } - - private async runPipes( - initialParams: unknown, - module: Module, - pipes: Type[] - ) { - let modifiedParams = initialParams; - for (const pipe of pipes) { - const pipeInstance = this.getPipeInstance(pipe, module); - modifiedParams = await pipeInstance.transform( - modifiedParams, - {} as ArgumentMetadata - ); - } - return modifiedParams; - } - - private getPipeInstance( - pipe: Type, - module: Module - ): PipeTransform { - const instanceWrapper = module.getProviderByKey(pipe); - if (!instanceWrapper) { - return this.moduleRef.get(pipe, { strict: false }); - } - return instanceWrapper.instance; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts deleted file mode 100644 index ec654d2d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Test } from '@nestjs/testing'; -import { ModulesContainer } from '@nestjs/core'; -import { - MAP_ENTITY, - MAP_CONTROLLER_ENTITY, - OPTIONS, - MAP_CONTROLLER_INTERCEPTORS, -} from '../constants'; -import { Operation } from '../utils'; -import { ExplorerService } from './explorer.service'; - -describe('ExplorerService', () => { - let service: ExplorerService; - class EntityName {} - class ControllerName {} - beforeEach(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [ - ExplorerService, - { - provide: ModulesContainer, - useValue: new Map([ - [ - 'TestModule', - { - controllers: new Map([['ControllerName', ControllerName]]), - }, - ], - ]), - }, - { - provide: MAP_ENTITY, - useValue: new Map([['EntityName', EntityName]]), - }, - { - provide: MAP_CONTROLLER_ENTITY, - useValue: new Map([[EntityName, ControllerName]]), - }, - { - provide: MAP_CONTROLLER_INTERCEPTORS, - useValue: new Map(), - }, - { - provide: OPTIONS, - useValue: {}, - }, - ], - }).compile(); - - service = moduleRef.get(ExplorerService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('getControllerByEntityName()', () => { - it('should return the correct controller for a given entity name', () => { - expect(service.getControllerByEntityName('EntityName')).toBeDefined(); - }); - }); - - describe('getMethodNameByParam()', () => { - it('should return the correct method name for given parameters', () => { - expect(service.getMethodNameByParam(Operation.add, 'id', 'rel')).toBe( - 'postRelationship' - ); - }); - }); - - describe('getParamsForMethod()', () => { - it('should return the correct parameters for a given method name', () => { - const data = { - ref: { - id: '1', - relationship: 'belongs-to', - type: 'TypeA', - }, - op: Operation.add, - data: {}, - }; - expect(service.getParamsForMethod('patchRelationship', data)).toEqual([ - data.ref.id, - data.ref.relationship, - { data: data.data }, - ]); - }); - }); - - describe('getModulesByController()', () => { - it('should return the correct module for a given controller', () => { - expect( - service.getModulesByController(ControllerName as any) - ).toBeDefined(); - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts deleted file mode 100644 index 582421ef..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Inject, Injectable, Type } from '@nestjs/common'; -import { Module } from '@nestjs/core/injector/module'; -import { ModulesContainer } from '@nestjs/core'; -import { MAP_CONTROLLER_ENTITY, MAP_ENTITY } from '../constants'; -import { MapController, MapEntity, OperationMethode } from '../types'; -import { Entity, EntityRelation } from '../../../types'; -import { InputArray, Operation } from '../utils'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; -import { - PatchData, - PatchRelationshipData, - PostData, - PostRelationshipData, -} from '../../../helper/zod'; - -@Injectable() -export class ExplorerService { - @Inject(ModulesContainer) - private readonly modulesContainer!: ModulesContainer; - - @Inject(MAP_ENTITY) private readonly mapEntity!: MapEntity; - @Inject(MAP_CONTROLLER_ENTITY) private readonly mapController!: MapController; - - private mapModuleByController = new Map< - Type>, - Module - >(); - - getControllerByEntityName(entityName: string): Type> { - const entity = this.mapEntity.get(entityName); - if (!entity) { - throw new Error(); - } - - const controller = this.mapController.get(entity); - if (!controller) { - throw new Error(); - } - - return controller; - } - - getMethodNameByParam( - operation: Operation, - id?: string, - rel?: string - ): OperationMethode { - switch (operation) { - case Operation.add: - return id ? 'postRelationship' : 'postOne'; - case Operation.remove: - return rel ? 'deleteRelationship' : 'deleteOne'; - case Operation.update: - return rel ? 'patchRelationship' : 'patchOne'; - default: - throw new Error(); - } - } - - getParamsForMethod( - methodName: OperationMethode, - data: InputArray[number] - ): Parameters[typeof methodName]> { - const { op, ref, ...other } = data; - switch (methodName) { - case 'postOne': - return [other as PostData]; - case 'patchOne': - return [ref.id as string, other as PatchData]; - case 'deleteOne': - return [ref.id as string]; - case 'deleteRelationship': - return [ - ref.id as string, - ref.relationship as EntityRelation, - other as PostRelationshipData, - ]; - case 'patchRelationship': - return [ - ref.id as string, - ref.relationship as EntityRelation, - other as PatchRelationshipData, - ]; - case 'postRelationship': - return [ - ref.id as string, - ref.relationship as EntityRelation, - other as PostRelationshipData, - ]; - } - } - - getModulesByController(controllers: Type>): Module { - const module = this.mapModuleByController.get(controllers); - if (module) { - return module; - } - - const findModule = [...this.modulesContainer.values()].find((i) => - [...i.controllers.values()].find((c) => c.name === controllers.name) - ); - if (findModule) { - return findModule; - } - - throw new Error(); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts deleted file mode 100644 index aaf75a95..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './explorer.service'; -export * from './execute.service'; -export * from './swagger.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts deleted file mode 100644 index cf263440..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; -import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { generateSchema } from '@anatine/zod-openapi'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; - -import { OperationController } from '../controllers'; -import { ZodInputOperation } from '../utils'; -import { ZOD_INPUT_OPERATION } from '../constants'; - -@Injectable() -export class SwaggerService implements OnModuleInit { - @Inject(ModuleRef) private readonly moduleRef!: ModuleRef; - @Inject(ZOD_INPUT_OPERATION) - private typeZodInputOperation!: ZodInputOperation; - - private initSwagger() { - const operationControllerInst = this.moduleRef.get(OperationController); - if (!operationControllerInst) - throw new Error('OperationController not found'); - const controller = operationControllerInst.constructor.prototype; - const descriptor = Reflect.getOwnPropertyDescriptor(controller, 'index'); - if (!descriptor) - throw new Error(`Descriptor for controller OperationController is empty`); - - ApiTags('Atomic operation')(operationControllerInst.constructor); - ApiOperation({ - summary: `Atomic operation for several entity"`, - operationId: `atomic_operation`, - })(controller, 'index', descriptor); - - ApiBody({ - description: `Json api schema for new atomic operatiom`, - schema: generateSchema(this.typeZodInputOperation) as - | SchemaObject - | ReferenceObject, - required: true, - examples: { - allField: { - summary: 'Examples several operation', - description: 'Examples several operation', - value: { - ['atomic:operations']: [ - { - op: 'add', - ref: { - type: 'users', - }, - data: 'EntityPostOne', - }, - { - op: 'update', - ref: { - type: 'users', - id: '1', - }, - data: 'EntityPatchOne', - }, - { - op: 'remove', - ref: { - type: 'users', - id: '1', - }, - }, - { - op: 'add', - ref: { - type: 'users', - id: '1', - relationship: 'EntityRelationName', - }, - data: 'UsersPostRelationship', - }, - { - op: 'update', - ref: { - type: 'users', - id: '1', - relationship: 'EntityRelationName', - }, - data: 'UsersDeleteRelationship', - }, - ], - }, - }, - }, - })(controller, 'index', descriptor); - } - - onModuleInit(): void { - this.initSwagger(); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts deleted file mode 100644 index f01175a9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NestInterceptor, Type } from '@nestjs/common'; -import { Module } from '@nestjs/core/injector/module'; -import { Controller } from '@nestjs/common/interfaces'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { Entity, MethodName } from '../../../types'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; - -export type MapControllerInterceptor = Map< - Controller, - Map<(...arg: any) => any, NestInterceptor[]> ->; -export type MapController = Map>; -export type MapEntity = Map; - -export type OperationMethode = keyof Omit< - { [k in MethodName]: string }, - 'getAll' | 'getOne' | 'getRelationship' ->; - -export type ParamsForExecute< - E extends Entity = Entity, - O extends OperationMethode = OperationMethode -> = { - methodName: O; - controller: Type>; - params: Parameters[O]>; - module: Module; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts deleted file mode 100644 index 405fa85d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './zod/zod-helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts deleted file mode 100644 index 49d36c01..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts +++ /dev/null @@ -1,575 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { DataSource } from 'typeorm'; -import { IMemoryDb } from 'pg-mem'; -import { z, ZodError } from 'zod'; -import { - Operation, - ZodAdd, - zodAdd, - zodInputOperation, - ZodInputOperation, - zodOperationRel, - ZodOperationRel, - zodRemove, - ZodRemove, - zodUpdate, - ZodUpdate, -} from './zod-helper'; -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../../mock-utils'; -import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; -import { JsonBaseController } from '../../../../mixin/controller/json-base.controller'; -import { MapController } from '../../types'; -import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; - -describe('ZodHelperSpec', () => { - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - describe('zodAdd', () => { - it('should be correct', () => { - const user = 'user'; - const schema = zodAdd(user); - const check: z.infer> = { - op: Operation.add, - ref: { - type: user, - }, - data: {}, - }; - const check1: z.infer> = { - op: Operation.add, - ref: { - type: user, - }, - data: [{}], - }; - const check2: z.infer> = { - op: Operation.add, - ref: { - type: user, - }, - data: null, - }; - const check3: z.infer> = { - op: Operation.add, - ref: { - type: user, - }, - data: null, - }; - const checkArray = [check, check1, check2, check3]; - for (const item of checkArray) { - const result = schema.parse(item); - expect(result.op).toBe(Operation.add); - expect(result.ref.type).toBe(user); - expect(result).toHaveProperty('data'); - } - }); - it('should be not correct', () => { - const schema = zodAdd('user'); - const check = { - op: Operation.add, - ref: { - type: 'user', - }, - data: {}, - sdfsf: {}, - }; - const check1 = { - op: Operation.add, - ref: { - type: 'user', - }, - }; - const check2 = { - op: Operation.add, - ref: { - type: 'user', - sdsdf: 'ssdfdsf', - }, - data: {}, - }; - const check3 = { - op: Operation.add, - ref: { - type12: 'user', - }, - data: {}, - }; - const check4 = { - op: Operation.add, - ref: { - type: 'sdfsdf', - }, - data: {}, - }; - const check5 = { - op: 'sdfsdf', - ref: { - type: 'user', - }, - data: {}, - }; - - const checkArray = [check, check1, check2, check3, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('zodUpdate', () => { - it('should be correct', () => { - const user = 'user'; - const schema = zodUpdate(user); - const check: z.infer> = { - op: Operation.update, - ref: { - type: 'user', - id: '1', - }, - data: {}, - }; - const checkArray = [check]; - for (const item of checkArray) { - const result = schema.parse(item); - expect(result.op).toBe(Operation.update); - expect(result.ref.type).toBe(user); - expect(result).toHaveProperty('data'); - } - }); - it('should be not correct', () => { - const schema = zodUpdate('user'); - const check = { - op: Operation.update, - ref: { - type: 'user', - id: '12', - }, - data: {}, - sdfsf: {}, - }; - const check1 = { - op: Operation.update, - ref: { - type: 'user', - id: '12', - }, - }; - const check2 = { - op: Operation.update, - ref: { - type: 'user', - id: '12', - sdsdf: 'ssdfdsf', - }, - data: {}, - }; - const check3 = { - op: Operation.update, - ref: { - type12: 'user', - id: '12', - }, - data: {}, - }; - const check4 = { - op: Operation.update, - ref: { - type: 'sdfsdf', - id: '12', - }, - data: {}, - }; - const check5 = { - op: 'sdfsdf', - ref: { - type: 'user', - id: '12', - }, - data: {}, - }; - const check6 = { - op: Operation.update, - ref: { - type: 'user', - }, - data: {}, - }; - - const checkArray = [ - check, - check1, - check2, - check3, - check4, - check5, - check6, - ]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('zodRemove', () => { - it('should be correct', () => { - const user = 'user'; - const schema = zodRemove(user); - const check: z.infer> = { - op: Operation.remove, - ref: { - type: 'user', - id: '1', - }, - }; - const checkArray = [check]; - for (const item of checkArray) { - const result = schema.parse(item); - expect(result.op).toBe(Operation.remove); - expect(result.ref.type).toBe(user); - expect(result).not.toHaveProperty('data'); - } - }); - - it('should be not correct', () => { - const schema = zodRemove('user'); - const check = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - }, - sdfsf: {}, - }; - const check1 = { - op: Operation.remove, - ref: { - type: 'user', - idsdf: '12', - }, - }; - const check2 = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - sdsdf: 'ssdfdsf', - }, - }; - const check3 = { - op: Operation.remove, - ref: { - type12: 'user', - id: '12', - }, - }; - const check4 = { - op: Operation.remove, - ref: { - type: 'sdfsdf', - id: '12', - }, - }; - const check5 = { - op: 'sdfsdf', - ref: { - type: 'user', - id: '12', - }, - }; - const check6 = { - op: Operation.remove, - ref: { - type: 'user', - }, - }; - - const checkArray = [ - check, - check1, - check2, - check3, - check4, - check5, - check6, - ]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('zodOperationRel', () => { - it('should be correct', () => { - const user = 'user'; - const rel: ['address', 'notes'] = ['address', 'notes']; - const schema = zodOperationRel(user, rel, Operation.remove); - const check: z.infer< - ZodOperationRel<'user', ['address', 'notes'], Operation.remove> - > = { - op: Operation.remove, - ref: { - type: 'user', - id: '1', - relationship: 'notes', - }, - data: { - id: 1, - type: 'notes', - }, - }; - const checkArray = [check]; - for (const item of checkArray) { - const result = schema.parse(item); - expect(result.op).toBe(Operation.remove); - expect(result.ref.type).toBe(user); - expect(result).toHaveProperty('data'); - expect(result['data']).toEqual(check.data); - } - }); - it('should be not correct', () => { - const user = 'user'; - const rel: ['address', 'notes'] = ['address', 'notes']; - const schema = zodOperationRel(user, rel, Operation.remove); - - const check = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - relationship: 'notes', - }, - data: {}, - sdfsf: {}, - }; - const check1 = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - relationship: 'notes', - sdfsdf: 'sdfsdf', - }, - data: {}, - }; - const check2 = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - relationship1: 'notes', - }, - data: {}, - }; - const check3 = { - op: Operation.remove, - ref: { - type12: 'user', - id: '12', - relationship: 'notes', - }, - data: {}, - }; - const check4 = { - op: Operation.remove, - ref: { - type: 'sdfsdf', - id: '12', - relationship: 'notes', - }, - data: {}, - }; - const check5 = { - op: 'sdfsdf', - ref: { - type: 'user', - id: '12', - relationship: 'notes', - }, - data: {}, - }; - const check6 = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - relationship: 'notes1', - }, - data: {}, - }; - - const checkArray = [ - check, - check1, - check2, - check3, - check4, - check5, - check6, - ]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('zodInputOperation', () => { - let db: IMemoryDb; - let dataSource: DataSource; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [...providerEntities(getDataSourceToken())], - }).compile(); - dataSource = module.get( - getDataSourceToken(DEFAULT_CONNECTION_NAME) - ); - }); - - it('should be correct', () => { - const mapController: MapController = new Map([ - [Users as any, JsonBaseController], - ]); - const schema = zodInputOperation(dataSource, mapController); - const check: z.infer = { - [KEY_MAIN_INPUT_SCHEMA]: [ - { - data: {}, - op: Operation.update, - ref: { - type: 'users', - relationship: 'manager', - id: '1', - }, - }, - { - data: {}, - op: Operation.update, - ref: { - type: 'users', - id: '1', - }, - }, - { - data: {}, - op: Operation.add, - ref: { - type: 'users', - }, - }, - { - op: Operation.remove, - ref: { - type: 'users', - id: '1', - }, - }, - ], - }; - expect(schema.parse(check)).toEqual(check); - }); - - it('incorrect input main data', () => { - const mapController: MapController = new Map([ - [Users as any, JsonBaseController], - ]); - const schema = zodInputOperation(dataSource, mapController); - const check = {}; - const check1 = { - ssdf: 'sdfsdf', - }; - const check2 = { - [KEY_MAIN_INPUT_SCHEMA]: null, - }; - const check3 = { - [KEY_MAIN_INPUT_SCHEMA]: '', - }; - const check4 = { - [KEY_MAIN_INPUT_SCHEMA]: {}, - }; - const check5 = { - [KEY_MAIN_INPUT_SCHEMA]: [], - }; - const checkArray = [check, check1, check2, check3, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - - it('should be incorrect methode not allow', () => { - class Test extends JsonBaseController { - override deleteOne(id: string | number): Promise { - return super.deleteOne(id); - } - } - const mapController: MapController = new Map([[Users as any, Test]]); - const schema = zodInputOperation(dataSource, mapController); - const check: z.infer = { - [KEY_MAIN_INPUT_SCHEMA]: [ - { - data: {}, - op: Operation.update, - ref: { - type: 'users', - relationship: 'manager', - id: '1', - }, - }, - ], - }; - const check1: z.infer = { - [KEY_MAIN_INPUT_SCHEMA]: [ - { - data: {}, - op: Operation.remove, - ref: { - type: 'users1', - relationship: 'manager', - id: '1', - }, - }, - ], - }; - const checkArray = [check, check1]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts deleted file mode 100644 index 19d6ce68..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { - z, - ZodArray, - ZodLiteral, - ZodNullable, - ZodNumber, - ZodObject, - ZodOptional, - ZodString, - ZodType, - ZodTypeAny, - ZodTypeDef, - ZodUnion, -} from 'zod'; -import { ZodUnionOptions } from 'zod/lib/types'; -import { DataSource } from 'typeorm'; - -import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; -import { MapController } from '../../types'; -import { UnionToTupleMain } from '../../../../types'; -import { EntityTarget } from 'typeorm/common/EntityTarget'; -import { camelToKebab, getEntityName } from '../../../../helper'; -import { getField } from '../../../../helper/orm'; - -export enum Operation { - add = 'add', - update = 'update', - remove = 'remove', -} - -const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); -type Literal = z.infer; -type Json = Literal | { [key: string]: Json } | Json[]; - -const jsonSchema: ZodType = z.lazy(() => - z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) -); - -type ZodGeneral = ZodNullable>; -const zodGeneralData: ZodGeneral = jsonSchema.nullable(); - -export type ZodAdd = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - tmpId: ZodOptional>; - }>; - data: ZodGeneral; -}>; -export const zodAdd = (type: T): ZodAdd => - z - .object({ - op: z.literal(Operation.add), - ref: z - .object({ - type: z.literal(type), - tmpId: z.union([z.number(), z.string()]).optional(), - }) - .strict(), - data: zodGeneralData, - }) - .strict(); - -export type ZodUpdate = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - id: ZodString; - }>; - data: ZodGeneral; -}>; -export const zodUpdate = (type: T): ZodUpdate => - z - .object({ - op: z.literal(Operation.update), - ref: z - .object({ - type: z.literal(type), - id: z.string(), - }) - .strict(), - data: zodGeneralData, - }) - .strict(); -export type ZodRemove = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - id: ZodString; - }>; -}>; -export const zodRemove = (type: T): ZodRemove => - z - .object({ - op: z.literal(Operation.remove), - ref: z - .object({ - type: z.literal(type), - id: z.string(), - }) - .strict(), - }) - .strict(); - -type RelToLiteralArray = UnionToTupleMain< - { - [K in Rel[number]]: ZodLiteral; - }[Rel[number]] ->; - -type RelLiteralArrayToUnion = T extends ZodUnionOptions - ? ZodUnion - : never; - -type ZodRelLiteral = RelLiteralArrayToUnion< - RelToLiteralArray ->; - -export type ZodOperationRel< - T extends string, - Rel extends [string, ...string[]], - OP extends Operation -> = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - id: ZodString; - relationship: ZodRelLiteral; - }>; - data: ZodGeneral; -}>; -export const zodOperationRel = < - T extends string, - R extends [string, ...string[]], - OP extends Operation ->( - type: T, - rel: R, - typeOperation: OP -): ZodOperationRel => { - const literalArray = rel.map((i) => z.literal(i)) as [ - ZodLiteral, - ZodLiteral, - ...ZodLiteral[] - ]; - - return z - .object({ - op: z.literal(typeOperation), - ref: z - .object({ - type: z.literal(type), - id: z.string(), - relationship: z.union(literalArray) as ZodRelLiteral, - }) - .strict(), - data: zodGeneralData, - }) - .strict(); -}; -export type ZodInputArray = ZodArray< - ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodString; - id: ZodOptional; - relationship: ZodOptional; - tmpId: ZodOptional>; - }>; - data: ZodOptional; - }>, - 'atleastone' ->; -export type InputArray = z.infer; - -export type ZodInputOperation = ZodObject< - { - [KEY_MAIN_INPUT_SCHEMA]: ZodInputArray; - }, - 'strict' ->; - -export const zodInputOperation = ( - dataSource: DataSource, - mapController: MapController -): ZodInputOperation => { - const array: [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]] = [] as any; - for (const [entity, controller] of mapController.entries()) { - type Entity = typeof entity; - const repository = dataSource.getRepository( - entity as EntityTarget - ); - - const typeName = camelToKebab(getEntityName(repository.target)); - const { relations } = getField(repository); - - const hasOwnProperty = (props: string) => - Object.prototype.hasOwnProperty.call(controller.prototype, props); - - if (hasOwnProperty('postOne')) { - array.push(zodAdd(typeName)); - } - if (hasOwnProperty('patchOne')) { - array.push(zodUpdate(typeName)); - } - if (hasOwnProperty('deleteOne')) { - array.push(zodRemove(typeName)); - } - if (hasOwnProperty('postRelationship')) { - array.push(zodOperationRel(typeName, relations, Operation.add)); - } - if (hasOwnProperty('deleteRelationship')) { - array.push(zodOperationRel(typeName, relations, Operation.remove)); - } - if (hasOwnProperty('patchRelationship')) { - array.push(zodOperationRel(typeName, relations, Operation.update)); - } - } - - return z - .object({ - [KEY_MAIN_INPUT_SCHEMA]: z.array(z.union(array)).nonempty(), - }) - .strict() as unknown as ZodInputOperation; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts deleted file mode 100644 index 60a52e3a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - UserGroups, - Users, -} from '../mock-utils'; -import { CurrentDataSourceProvider } from '../factory'; -import { DEFAULT_CONNECTION_NAME } from '../constants'; -import { EntityPropsMapService } from './entity-props-map.service'; - -describe('EntityPropsMapService', () => { - let db: IMemoryDb; - let entityPropsMapService: EntityPropsMapService; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityPropsMapService, - ], - }).compile(); - - entityPropsMapService = module.get( - EntityPropsMapService - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('getPropsForEntity', () => { - const result = entityPropsMapService.getRelPropsForEntity(Users); - expect(result).toEqual([ - 'addresses', - 'manager', - 'roles', - 'comments', - 'notes', - 'userGroup', - ]); - }); - - it('getPropsForEntity', () => { - const result = entityPropsMapService.getPropsForEntity(Users); - expect(result).toEqual([ - 'id', - 'login', - 'firstName', - 'testReal', - 'testArrayNull', - 'lastName', - 'isActive', - 'createdAt', - 'testDate', - 'updatedAt', - ]); - }); - - it('getPrimaryColumnsForEntity', () => { - expect(entityPropsMapService.getPrimaryColumnsForEntity(Users)).toBe('id'); - }); - - it('getNameForEntity', () => { - expect(entityPropsMapService.getNameForEntity(Users)).toBe('Users'); - expect(entityPropsMapService.getNameForEntity(UserGroups)).toBe( - 'UserGroups' - ); - }); - - it('getRelationPropsType', () => { - expect( - entityPropsMapService.getRelationPropsType(Users, 'userGroup' as any) - ).toEqual(UserGroups); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts b/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts deleted file mode 100644 index 00263a92..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, EntityTarget } from 'typeorm'; - -import { CURRENT_DATA_SOURCE_TOKEN } from '../constants'; -import { Entity, EntityRelation } from '../types'; -import { - getField, - ResultGetField, - TupleOfEntityProps, - TupleOfEntityRelation, -} from '../helper/orm'; - -@Injectable() -export class EntityPropsMapService { - @Inject(CURRENT_DATA_SOURCE_TOKEN) private dataSource!: DataSource; - - private _propsForEntity: Map = new Map(); - private _relPropsForEntity: Map = new Map(); - private _relTypePropsForEntity: Map = new Map(); - private _primaryColumnsForEntity: Map = new Map(); - private _nameForEntity: Map = new Map(); - - getPropsForEntity(entity: E): TupleOfEntityProps { - const result = this._propsForEntity.get(entity); - if (result) { - return result; - } - - const { field } = this.pullPropsAndRelFoEntity(entity); - - return field; - } - - getRelPropsForEntity(entity: E): TupleOfEntityRelation { - const result = this._relPropsForEntity.get(entity); - if (result) { - return result; - } - - const { relations } = this.pullPropsAndRelFoEntity(entity); - - return relations; - } - - getRelationPropsType(entity: E, rel: EntityRelation) { - const result = this._relTypePropsForEntity.get(entity); - if (result) { - return result[rel]; - } - - const repo = this.dataSource.getRepository(entity as EntityTarget); - const relToType = repo.metadata.relations.reduce((acum, item) => { - acum[item.propertyName as keyof E] = item.inverseEntityMetadata.target; - return acum; - }, {} as Record); - - this._relTypePropsForEntity.set(entity, relToType); - return relToType[rel]; - } - - getPrimaryColumnsForEntity(entity: E): keyof E { - const result = this._primaryColumnsForEntity.get(entity); - if (result) { - return result as keyof E; - } - const primaryColumns = this.dataSource.getRepository( - entity as EntityTarget - ).metadata.primaryColumns[0].propertyName; - this._primaryColumnsForEntity.set(entity, primaryColumns); - - return primaryColumns as keyof E; - } - - getNameForEntity(entity: E): string { - const result = this._nameForEntity.get(entity); - if (result) { - return result; - } - const name = this.dataSource.getRepository(entity as EntityTarget) - .metadata.name; - this._nameForEntity.set(entity, name); - return name; - } - - private pullPropsAndRelFoEntity( - entity: E - ): ResultGetField { - const repo = this.dataSource.getRepository(entity as EntityTarget); - - const { relations, field } = getField(repo); - this._propsForEntity.set(entity, field); - this._relPropsForEntity.set(entity, relations); - - return { relations, field }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/service/index.ts deleted file mode 100644 index 952eb04b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './transform-input.service'; -export * from '../mixin/service/typeorm-utils.service'; -export * from './entity-props-map.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts deleted file mode 100644 index 01fcf175..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { TransformInputService } from './transform-input.service'; -import { ASC, DESC } from '../constants'; -import { Users } from '../mock-utils'; -import { QueryField, TypeInputProps } from '../helper'; - -describe('TransformInputService', () => { - let transformInputService: TransformInputService; - - beforeAll(() => { - transformInputService = new TransformInputService(); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('TransformInputService.transformSort', () => { - const check = 'id,manager.id,-roles.id,updatedAt,-createdAt'; - const checkResult = { - target: { - id: ASC, - updatedAt: ASC, - createdAt: DESC, - }, - manager: { - id: ASC, - }, - roles: { - id: DESC, - }, - }; - - const check2 = 'id'; - const checkResult2 = { - target: { - id: ASC, - }, - }; - - const check3 = ''; - - const result = transformInputService.transformSort(check); - const result2 = transformInputService.transformSort(undefined); - const result3 = transformInputService.transformSort(check2); - const result4 = transformInputService.transformSort(check3); - expect(result).toEqual(checkResult); - expect(result2).toBe(null); - expect(result4).toBe(null); - }); - - it('TransformInputService.transformInclude', () => { - const check = 'manager,roles,,notes,'; - const checkResult = ['manager', 'roles', 'notes']; - - const check2 = ''; - - const check3 = 'manager'; - const checkResult3 = ['manager']; - - const result = transformInputService.transformInclude(check); - const result2 = transformInputService.transformInclude(undefined); - const result3 = transformInputService.transformInclude(check2); - const result4 = transformInputService.transformInclude(check3); - expect(result).toEqual(checkResult); - expect(result2).toBe(null); - expect(result3).toBe(null); - expect(result4).toEqual(checkResult3); - }); - - it('TransformInputService.transformFields', () => { - const check = { - target: 'id,updatedAt,createdAt', - manager: 'id', - roles: 'id,key', - }; - const check2 = { - target: 'id,updatedAt,createdAt', - }; - const check3 = { - roles: 'id,key', - }; - const check4 = { - target: '', - roles: '', - }; - const check5 = { - target: 'id,updatedAt,createdAt', - roles: '', - }; - const checkResult = { - target: ['id', 'updatedAt', 'createdAt'], - manager: ['id'], - roles: ['id', 'key'], - }; - const checkResult2 = { - target: ['id', 'updatedAt', 'createdAt'], - }; - const checkResult3 = { - roles: ['id', 'key'], - }; - - const checkResult5 = { - target: ['id', 'updatedAt', 'createdAt'], - }; - - const result = transformInputService.transformFields(check); - const result2 = transformInputService.transformFields(undefined); - const result3 = transformInputService.transformFields({}); - const result4 = transformInputService.transformFields(check2); - const result5 = transformInputService.transformFields(check3 as any); - const result6 = transformInputService.transformFields(check4 as any); - const result7 = transformInputService.transformFields(check5 as any); - expect(result).toEqual(checkResult); - expect(result2).toBe(null); - expect(result3).toBe(null); - expect(result4).toEqual(checkResult2); - expect(result5).toEqual(checkResult3); - expect(result6).toEqual(null); - expect(result7).toEqual(checkResult5); - }); - - it('TransformInputService.transformFilter', () => { - const check1: TypeInputProps = { - id: { - in: 'in-test', - nin: 'nin-test,nin-test2', - }, - isActive: 'true', - manager: { - eq: 'null', - }, - 'addresses.arrayField': { - some: 'some-test', - }, - 'addresses.createdAt': '2023-01-01', - 'userGroup.label': { - like: 'test', - }, - }; - const checkResult1 = { - target: { - id: { - in: ['in-test'], - nin: ['nin-test', 'nin-test2'], - }, - isActive: { - eq: 'true', - }, - manager: { - eq: 'null', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['some-test'], - }, - createdAt: { - eq: '2023-01-01', - }, - }, - userGroup: { - label: { like: 'test' }, - }, - }, - }; - - const check2: TypeInputProps = { - id: '', - 'addresses.arrayField': {}, - 'addresses.createdAt': '2023-01-01', - 'userGroup.label': {}, - }; - const checkResult2 = { - target: { - id: { - eq: '', - }, - }, - relation: { - addresses: { - createdAt: { - eq: '2023-01-01', - }, - }, - }, - }; - - const result1 = transformInputService.transformFilter(check1); - expect(result1).toEqual(checkResult1); - - const result2 = transformInputService.transformFilter(check2); - expect(result2).toEqual(checkResult2); - - expect(transformInputService.transformFilter(undefined)).toEqual({ - target: null, - relation: null, - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts b/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts deleted file mode 100644 index c9aadbde..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { Entity, FilterOperand } from '../types'; -import { isString } from '../helper/utils'; -import { QueryField, TypeInputProps } from '../helper/zod'; - -import { ASC, DESC } from '../constants'; - -const arrayOp = { - [FilterOperand.in]: true, - [FilterOperand.nin]: true, - [FilterOperand.some]: true, -}; -function convertToFilterObject( - value: Record | string -): Partial<{ - [key in FilterOperand]: string | string[]; -}> { - if (isString | string, string>(value)) { - return { - [FilterOperand.eq]: value, - }; - } else { - return Object.entries(value).reduce((acum, [op, filed]) => { - if (op in arrayOp) { - acum[op] = (isString(filed) ? filed.split(',') : []).filter((i) => !!i); - } else { - acum[op] = filed; - } - return acum; - }, {} as Record); - } -} - -type OutPutFilter = { - relation: null | Record< - string, - Record< - string, - Partial<{ - [key in FilterOperand]: string | string[]; - }> - > - >; - target: null | Record< - string, - Partial<{ - [key in FilterOperand]: string | string[]; - }> - >; -}; - -Injectable(); -export class TransformInputService { - public transformSort( - data: TypeInputProps - ): Record> | null { - if (!data) return null; - return data - .split(',') - .map((i) => i.trim()) - .filter((i) => !!i) - .reduce((acum, field) => { - const fieldName = field.charAt(0) === '-' ? field.substring(1) : field; - const sort = field.charAt(0) === '-' ? DESC : ASC; - if (fieldName.indexOf('.') > -1) { - const [relation, fieldRelation] = field.split('.'); - const relationName = - relation.charAt(0) === '-' ? relation.substring(1) : relation; - - acum[relationName] = acum[relationName] || {}; - acum[relationName][fieldRelation] = sort; - } else { - acum['target'] = acum['target'] || {}; - acum['target'][fieldName] = sort; - } - - return acum; - }, {} as Record>); - } - - public transformInclude( - data: TypeInputProps - ): string[] | null { - if (!data || !isString(data)) return null; - return data - .split(',') - .map((i) => i.trim()) - .filter((i) => !!i); - } - - transformFields( - data: TypeInputProps - ): Record | null { - if (!data) return null; - - const prepareResult = Object.entries(data).reduce((acum, [key, val]) => { - acum[key] = val - .split(',') - .map((i: string) => i.trim()) - .filter((i: string) => !!i); - return acum; - }, {} as Record); - - const result = Object.entries(prepareResult).reduce( - (acum, [key, value]) => { - if (value.length > 0) { - acum[key] = value; - } - return acum; - }, - {} as Record - ); - - return Object.keys(result).length > 0 ? result : null; - } - - transformFilter( - data: TypeInputProps - ): OutPutFilter | null { - if (!data) { - return { - relation: null, - target: null, - }; - } - return Object.entries(data).reduce( - (acum, [field, value]: [string, any]) => { - const objectOperand = convertToFilterObject(value); - if (Object.keys(objectOperand).length === 0) { - return acum; - } - - if (field.indexOf('.') > -1) { - const [relation, fieldRelation] = field.split('.'); - acum['relation'] = !acum['relation'] ? {} : acum['relation']; - acum['relation'][relation] = acum['relation'][relation] || {}; - acum['relation'][relation][fieldRelation] = objectOperand; - } else { - acum['target'] = !acum['target'] ? {} : acum['target']; - acum['target'][field] = objectOperand; - } - - return acum; - }, - { relation: null, target: null } as OutPutFilter - ); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts deleted file mode 100644 index 21f6d4fb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PipeTransform, RequestMethod } from '@nestjs/common'; -import { Type } from '@nestjs/common/interfaces'; -import { PipeFabric } from './module.types'; -import { JsonBaseController } from '../mixin'; - -export type MethodName = - | 'getAll' - | 'getOne' - | 'getRelationship' - | 'deleteOne' - | 'deleteRelationship' - | 'postOne' - | 'postRelationship' - | 'patchOne' - | 'patchRelationship'; - -type MapNameToTypeMethod = { - getAll: RequestMethod.GET; - getOne: RequestMethod.GET; - patchOne: RequestMethod.PATCH; - patchRelationship: RequestMethod.PATCH; - postOne: RequestMethod.POST; - postRelationship: RequestMethod.POST; - deleteOne: RequestMethod.DELETE; - deleteRelationship: RequestMethod.DELETE; - getRelationship: RequestMethod.GET; -}; - -export interface Binding { - path: string; - method: MapNameToTypeMethod[T]; - name: T; - implementation: JsonBaseController[T]; - parameters: { - decorator: ( - property?: string, - ...pipes: (Type | PipeTransform)[] - ) => ParameterDecorator; - property?: string; - mixins: PipeFabric[]; - }[]; -} - -export type BindingsConfig = { - [Key in MethodName]: Binding; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts deleted file mode 100644 index 680c34ce..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MethodName } from './binding.types'; -import { ConfigParam } from './module.types'; - -export type DecoratorOptions = Partial< - { - allowMethod: Array; - } & ConfigParam ->; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts deleted file mode 100644 index ca9e3564..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ZodIssue } from 'zod'; - -export type InnerErrorType = - | 'invalid_arguments' - | 'unrecognized_keys' - | 'internal_error'; - -export type InnerError = { - code: InnerErrorType; - message: string; - path: string[]; - keys?: string[]; - error?: Error; -}; - -export type ValidateQueryError = ZodIssue | InnerError; - -export type ErrorDescribe = ValidateQueryError; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/types/index.ts deleted file mode 100644 index 43941180..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './module.types'; -export * from './binding.types'; -export * from './decorator-options.types'; -export * from './utils'; -export * from './operand'; -export * from './error.types'; -export * from './typeorm-service.type'; -export * from './response'; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/module.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/module.types.ts deleted file mode 100644 index 083a56da..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/module.types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ModuleMetadata, Type, PipeTransform } from '@nestjs/common'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; -import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; - -export type NestController = ModuleMetadata['controllers']; -export type NestProvider = ModuleMetadata['providers']; -export type NestImport = ModuleMetadata['imports']; -export type PipeMixin = Type; -export type Entity = EntityClassOrSchema | ObjectLiteral; - -export type PipeFabric = ( - entity: Entity, - connectionName: string, - config?: ConfigParam -) => PipeMixin; - -export type ExtractNestType = - ArrayType extends readonly (infer ElementType)[] ? ElementType : never; - -export interface ConfigParam { - requiredSelectField: boolean; - debug: boolean; - useSoftDelete: boolean; - pipeForId: PipeMixin; - operationUrl?: string; - overrideRoute?: string; - runInTransaction?: any>( - isolationLevel: IsolationLevel, - fn: Func - ) => ReturnType; -} - -export interface ModuleOptions { - entities: EntityClassOrSchema[]; - controllers?: NestController; - connectionName?: string; - providers?: NestProvider; - options?: Partial; - imports?: NestImport; -} - -export interface BaseModuleOptions { - entity: EntityClassOrSchema; - connectionName: string; - controller?: ExtractNestType; - config: ConfigParam; - imports?: NestImport; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts b/libs/json-api/json-api-nestjs/src/lib/types/operand.ts deleted file mode 100644 index 10ace773..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { FilterOperand } from 'json-shared-type'; - -export { FilterOperand }; -export const EXPRESSION = 'EXPRESSION'; -export const OperandsMapExpression = { - [FilterOperand.eq]: `= :${EXPRESSION}`, - [FilterOperand.ne]: `<> :${EXPRESSION}`, - [FilterOperand.regexp]: `~* :${EXPRESSION}`, - [FilterOperand.gt]: `> :${EXPRESSION}`, - [FilterOperand.gte]: `>= :${EXPRESSION}`, - [FilterOperand.in]: `IN (:...${EXPRESSION})`, - [FilterOperand.like]: `ILIKE :${EXPRESSION}`, - [FilterOperand.lt]: `< :${EXPRESSION}`, - [FilterOperand.lte]: `<= :${EXPRESSION}`, - [FilterOperand.nin]: `NOT IN (:...${EXPRESSION})`, - [FilterOperand.some]: `&& :${EXPRESSION}`, -}; - -export const OperandMapExpressionForNull = { - [FilterOperand.ne]: 'IS NOT NULL', - [FilterOperand.eq]: 'IS NULL', -}; - -export const OperandsMapExpressionForNullRelation = { - [FilterOperand.ne]: `EXISTS ${EXPRESSION}`, - [FilterOperand.eq]: `NOT EXISTS ${EXPRESSION}`, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/response.ts b/libs/json-api/json-api-nestjs/src/lib/types/response.ts deleted file mode 100644 index 33f67dab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/response.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { - PageProps, - DebugMetaProps, - MainData, - Links, - Attributes, - Data, - Relationships, - Include, - ResourceData, - ResourceObject, - ResourceObjectRelationships, -} from 'json-shared-type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts b/libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts deleted file mode 100644 index 8062c965..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Repository } from 'typeorm'; - -import { ConfigParam, Entity } from './'; -import type { MethodsService } from '../helper'; -import { TypeormUtilsService } from '../service'; -import { TransformDataService } from '../mixin/service'; - -export type TypeormServiceObject = { - repository: Repository; - config: ConfigParam; - typeormUtilsService: TypeormUtilsService; - transformDataService: TransformDataService; -}; - -export type TypeormService = MethodsService; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/utils.ts b/libs/json-api/json-api-nestjs/src/lib/types/utils.ts deleted file mode 100644 index 87021b27..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Type } from '@nestjs/common/interfaces'; -import { EntityField, EntityProps, EntityRelation } from 'json-shared-type'; - -import { Entity } from './module.types'; - -export { EntityField, EntityProps, EntityRelation }; - -export type EntityPropsArray = { - [P in keyof T]: T[P] extends EntityField - ? IsArray extends true - ? P - : never - : never; -}[keyof T]; - -type UnionToIntersection = ( - U extends never ? never : (arg: U) => never -) extends (arg: infer I) => void - ? I - : never; - -export type UnionToTupleMain = UnionToIntersection< - T extends never ? never : (t: T) => T -> extends (_: never) => infer W - ? UnionToTupleMain, [...A, W]> - : A; - -export type UnionToTuple = UnionToTupleMain extends readonly [ - string, - ...string[] -] - ? UnionToTupleMain - : never; - -export type TypeCast = A extends T ? A : never; - -export type TypeOfArray = T extends (infer U)[] ? U : T; - -export type CastProps = K extends keyof E ? E[K] : never; - -export type TypeFromType = T extends Type ? A : never; - -export type ConcatStringArray = T extends [ - infer F extends string, - ...infer R extends string[] -] - ? `${F}${ConcatStringArray}` - : ''; - -export type Concat = ConcatStringArray< - [E, '.', F] ->; - -export type ValueOf = T[keyof T]; - -export type JSONValue = - | string - | number - | boolean - | null - | { [x: string]: JSONValue } - | Array; - -export type IsArray = [Extract] extends [never] ? false : true; diff --git a/libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json b/libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json deleted file mode 100644 index 5857b698..00000000 --- a/libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "types": ["node"], - "module": "es2015", - "target": "ES2022", - "removeComments": false, - "declaration": true, - "declarationMap": true - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/json-api/json-shared-type/.eslintrc.json b/libs/json-api/json-shared-type/.eslintrc.json deleted file mode 100644 index 3456be9b..00000000 --- a/libs/json-api/json-shared-type/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/libs/json-api/json-shared-type/README.md b/libs/json-api/json-shared-type/README.md deleted file mode 100644 index db1021f7..00000000 --- a/libs/json-api/json-shared-type/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# json-shared-type - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test json-shared-type` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/json-api/json-shared-type/jest.config.ts b/libs/json-api/json-shared-type/jest.config.ts deleted file mode 100644 index 2c9b7818..00000000 --- a/libs/json-api/json-shared-type/jest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'json-shared-type', - preset: '../../../jest.preset.js', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../../coverage/libs/json-api/json-shared-type', -}; diff --git a/libs/json-api/json-shared-type/project.json b/libs/json-api/json-shared-type/project.json deleted file mode 100644 index 6362de6b..00000000 --- a/libs/json-api/json-shared-type/project.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "json-shared-type", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/json-api/json-shared-type/src", - "projectType": "library", - "targets": { - "build1": { - "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/libs/shared-utils", - "tsConfig": "libs/shared-utils/tsconfig.lib.json", - "packageJson": "libs/shared-utils/package.json", - "main": "libs/shared-utils/src/index.ts", - "assets": ["libs/shared-utils/*.md"] - } - } - }, - "tags": [] -} diff --git a/libs/json-api/json-shared-type/src/index.ts b/libs/json-api/json-shared-type/src/index.ts deleted file mode 100644 index fcb073fe..00000000 --- a/libs/json-api/json-shared-type/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './types'; diff --git a/libs/json-api/json-shared-type/src/types/entity-type.ts b/libs/json-api/json-shared-type/src/types/entity-type.ts deleted file mode 100644 index 5fbdd04f..00000000 --- a/libs/json-api/json-shared-type/src/types/entity-type.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type EntityField = - | string - | number - | bigint - | boolean - | string[] - | number[] - | null - | Date; - -export type EntityProps = { - [P in keyof T]: T[P] extends EntityField ? P : never; -}[keyof T]; - -export type EntityRelation = { - [P in keyof T]: T[P] extends EntityField ? never : P; -}[keyof T]; diff --git a/libs/json-api/json-shared-type/src/types/index.ts b/libs/json-api/json-shared-type/src/types/index.ts deleted file mode 100644 index 8b86a958..00000000 --- a/libs/json-api/json-shared-type/src/types/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './entity-type'; -export * from './query-type'; -export * from './utils-type'; -export * from './response-body'; diff --git a/libs/json-api/json-shared-type/src/types/query-type.ts b/libs/json-api/json-shared-type/src/types/query-type.ts deleted file mode 100644 index dba33ee2..00000000 --- a/libs/json-api/json-shared-type/src/types/query-type.ts +++ /dev/null @@ -1,21 +0,0 @@ -export enum QueryField { - filter = 'filter', - sort = 'sort', - include = 'include', - page = 'page', - fields = 'fields', -} - -export enum FilterOperand { - eq = 'eq', - gt = 'gt', - gte = 'gte', - in = 'in', - like = 'like', - lt = 'lt', - lte = 'lte', - ne = 'ne', - nin = 'nin', - regexp = 'regexp', - some = 'some', -} diff --git a/libs/json-api/json-shared-type/src/types/response-body.ts b/libs/json-api/json-shared-type/src/types/response-body.ts deleted file mode 100644 index fbe91dde..00000000 --- a/libs/json-api/json-shared-type/src/types/response-body.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - EntityField, - EntityProps, - EntityRelation, - TypeOfArray, - ValueOf, -} from '.'; - -export type PageProps = { - totalItems: number; - pageNumber: number; - pageSize: number; -}; - -export type DebugMetaProps = Partial<{ - time: number; -}>; - -export type MainData = { - type: T; - id: string; -}; - -export type Links = { - self: string; - related?: string; -}; - -export type Attributes = { - [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; -}; - -export type Data = { - data?: E extends unknown[] ? MainData[] : MainData | null; -}; - -export type Relationships = { - [P in EntityRelation]?: { - links: Links; - } & Data; -}; - -export type Include = ValueOf<{ - [P in EntityRelation]: ResourceData>; -}>; - -export type ResourceData = MainData & { - attributes?: Attributes; - relationships?: Relationships; - links: Omit; -}; - -export type MetaProps = R extends null ? T : T & R; - -export type ResourceObject< - T, - R extends 'object' | 'array' = 'object', - M = null -> = { - meta: R extends 'array' - ? MetaProps - : MetaProps; - data: R extends 'array' ? ResourceData[] : ResourceData; - included?: Include[]; -}; - -export type ResourceObjectRelationships> = { - meta: DebugMetaProps; -} & Required>; diff --git a/libs/json-api/json-shared-type/src/types/utils-type.ts b/libs/json-api/json-shared-type/src/types/utils-type.ts deleted file mode 100644 index ef480a55..00000000 --- a/libs/json-api/json-shared-type/src/types/utils-type.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type TypeOfArray = T extends (infer U)[] ? U : T; - -export type ValueOf = T[keyof T]; diff --git a/libs/json-api/json-shared-type/tsconfig.json b/libs/json-api/json-shared-type/tsconfig.json deleted file mode 100644 index 8122543a..00000000 --- a/libs/json-api/json-shared-type/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/libs/json-api/json-shared-type/tsconfig.lib.json b/libs/json-api/json-shared-type/tsconfig.lib.json deleted file mode 100644 index 4befa7f0..00000000 --- a/libs/json-api/json-shared-type/tsconfig.lib.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/json-api/json-shared-type/tsconfig.spec.json b/libs/json-api/json-shared-type/tsconfig.spec.json deleted file mode 100644 index 69a251f3..00000000 --- a/libs/json-api/json-shared-type/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/libs/shared-utils/.eslintrc.json b/libs/shared-utils/.eslintrc.json deleted file mode 100644 index 9d9c0db5..00000000 --- a/libs/shared-utils/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/libs/shared-utils/README.md b/libs/shared-utils/README.md deleted file mode 100644 index 038826ff..00000000 --- a/libs/shared-utils/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# shared-utils - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test shared-utils` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared-utils/jest.config.ts b/libs/shared-utils/jest.config.ts deleted file mode 100644 index f2d2f024..00000000 --- a/libs/shared-utils/jest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'shared-utils', - preset: '../../jest.preset.js', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/libs/shared-utils', -}; diff --git a/libs/shared-utils/project.json b/libs/shared-utils/project.json deleted file mode 100644 index f67eee36..00000000 --- a/libs/shared-utils/project.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "shared-utils", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/shared-utils/src", - "projectType": "library", - "targets": { - "build1": { - "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/libs/shared-utils", - "tsConfig": "libs/shared-utils/tsconfig.lib.json", - "packageJson": "libs/shared-utils/package.json", - "main": "libs/shared-utils/src/index.ts", - "assets": ["libs/shared-utils/*.md"] - } - }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "options": { - "jestConfig": "libs/shared-utils/jest.config.ts" - } - } - }, - "tags": [] -} diff --git a/libs/shared-utils/src/index.ts b/libs/shared-utils/src/index.ts deleted file mode 100644 index a0fe9b9f..00000000 --- a/libs/shared-utils/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/utils'; -export * from './lib/types'; diff --git a/libs/shared-utils/src/lib/types/index.ts b/libs/shared-utils/src/lib/types/index.ts deleted file mode 100644 index c0f762d2..00000000 --- a/libs/shared-utils/src/lib/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './utils-string.type'; diff --git a/libs/shared-utils/src/lib/types/utils-string.type.ts b/libs/shared-utils/src/lib/types/utils-string.type.ts deleted file mode 100644 index d5719e31..00000000 --- a/libs/shared-utils/src/lib/types/utils-string.type.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type KebabCase = S extends `${infer C}${infer T}` - ? KebabCase extends infer U - ? U extends string - ? T extends Uncapitalize - ? `${Uncapitalize}${U}` - : `${Uncapitalize}-${U}` - : never - : never - : S; - -export type KebabToCamelCase = - S extends `${infer T}-${infer U}-${infer V}` - ? `${T}${Capitalize}${Capitalize>}` - : S extends `${infer T}-${infer U}` - ? `${Capitalize}${Capitalize>}` - : S; diff --git a/libs/shared-utils/src/lib/utils/index.ts b/libs/shared-utils/src/lib/utils/index.ts deleted file mode 100644 index a7799257..00000000 --- a/libs/shared-utils/src/lib/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './string-utils'; -export * from './object-utils'; diff --git a/libs/shared-utils/src/lib/utils/object-utils.ts b/libs/shared-utils/src/lib/utils/object-utils.ts deleted file mode 100644 index 461455b3..00000000 --- a/libs/shared-utils/src/lib/utils/object-utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const ObjectTyped = { - keys: Object.keys as (yourObject: T) => Array, - values: Object.values as (yourObject: U) => Array, - entries: Object.entries as ( - yourObject: O - ) => Array<[keyof O, O[keyof O]]>, - fromEntries: Object.fromEntries as ( - yourObjectEntries: [K, V][] - ) => Record, -}; - -export function isObject(item: unknown): item is object { - return typeof item === 'object' && !Array.isArray(item) && item !== null; -} diff --git a/libs/shared-utils/src/lib/utils/string-utils.spec.ts b/libs/shared-utils/src/lib/utils/string-utils.spec.ts deleted file mode 100644 index 437de652..00000000 --- a/libs/shared-utils/src/lib/utils/string-utils.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { camelToKebab, snakeToCamel, isString, kebabToCamel } from './'; - -describe('Test utils', () => { - it('camelToKebab', () => { - const result = camelToKebab('ApproverGroups'); - const result1 = camelToKebab('Users'); - - expect(result).toBe('approver-groups'); - expect(result1).toBe('users'); - }); - - it('snakeToCamel', () => { - const result = snakeToCamel('test_test'); - const result1 = snakeToCamel('test-test'); - const result2 = snakeToCamel('testTest'); - const result3 = snakeToCamel('event_incident_typeFK'); - expect(result).toBe('testTest'); - expect(result1).toBe('testTest'); - expect(result2).toBe('testTest'); - expect(result3).toBe('eventIncidentTypeFK'); - }); - - it('isString', () => { - expect(isString('string')).toBe(true); - expect(isString(String('string'))).toBe(true); - expect(isString(new Date())).toBe(false); - expect(isString(class {})).toBe(false); - }); - - it('kebabToCamel', () => { - const type = 'users-group'; - const type1 = 'users'; - - expect(kebabToCamel(type)).toBe('UsersGroup'); - expect(kebabToCamel(type1)).toBe('Users'); - }); -}); diff --git a/libs/shared-utils/src/lib/utils/string-utils.ts b/libs/shared-utils/src/lib/utils/string-utils.ts deleted file mode 100644 index 3b678710..00000000 --- a/libs/shared-utils/src/lib/utils/string-utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { KebabToCamelCase, KebabCase } from '../types'; - -export function isString(value: T): value is P { - return typeof value === 'string' || value instanceof String; -} - -export function snakeToCamel(str: string): string { - if (!str.match(/[\s_-]/g)) { - return str; - } - return str.replace(/([-_][a-z])/g, (group) => - group.toUpperCase().replace('-', '').replace('_', '') - ); -} - -export function camelToKebab(string: S): KebabCase { - return string - .replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1') - .toLowerCase() as KebabCase; -} - -export function upperFirstLetter(string: S): Capitalize { - return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize; -} - -export function kebabToCamel(str: S): KebabToCamelCase { - return str - .split('-') - .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) - .join('') as KebabToCamelCase; -} - -export function capitalizeFirstChar(str: string) { - return str - .split('-') - .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) - .join(''); -} diff --git a/libs/shared-utils/tsconfig.json b/libs/shared-utils/tsconfig.json deleted file mode 100644 index f5b85657..00000000 --- a/libs/shared-utils/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/libs/shared-utils/tsconfig.lib.json b/libs/shared-utils/tsconfig.lib.json deleted file mode 100644 index 33eca2c2..00000000 --- a/libs/shared-utils/tsconfig.lib.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "declaration": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/shared-utils/tsconfig.spec.json b/libs/shared-utils/tsconfig.spec.json deleted file mode 100644 index 9b2a121d..00000000 --- a/libs/shared-utils/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} From f22bf49665eb52ac6a59d8a0b3fe45dc3f4144b6 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 06:49:09 +0100 Subject: [PATCH 02/26] refactor(json-api-nestjs): Deep refactoring BREAKING CHANGE: Now relation body params allow only as specification. https://jsonapi.org/format/#crud-updating-resource-relationships befoare allow without data props --- .verdaccio/config.yml | 28 + docker-compose.yaml | 51 ++ .../src/lib/service/json-api-utils.service.ts | 2 +- .../src/lib/types/query-params.ts | 2 +- .../json-api-nestjs-shared/.eslintrc.json | 30 + .../json-api/json-api-nestjs-shared/README.md | 11 + .../json-api-nestjs-shared/jest.config.ts | 10 + .../json-api-nestjs-shared/package.json | 10 + .../json-api-nestjs-shared/project.json | 33 + .../json-api-nestjs-shared/src/index.ts | 2 + .../src/lib/types/entity-type.ts | 17 + .../src/lib/types/index.ts | 4 + .../src/lib/types/query-type.ts | 36 + .../src/lib/types/response-body.ts | 69 ++ .../src/lib/types/utils-string.type.ts | 20 + .../src/lib/utils/index.ts | 2 + .../src/lib/utils/object-utils.ts | 14 + .../src/lib/utils/string-utils.spec.ts | 42 + .../src/lib/utils/string-utils.ts | 38 + .../json-api-nestjs-shared/tsconfig.json | 22 + .../json-api-nestjs-shared/tsconfig.lib.json | 16 + .../json-api-nestjs-shared/tsconfig.spec.json | 15 + libs/json-api/json-api-nestjs/.eslintrc.json | 11 +- libs/json-api/json-api-nestjs/project.json | 67 +- libs/json-api/json-api-nestjs/src/index.ts | 23 + .../src/lib/constants/default.ts | 4 + .../json-api-nestjs/src/lib/constants/di.ts | 28 + .../src/lib/constants/index.ts | 14 + .../src/lib/constants/reflection.ts | 2 + .../src/lib/json-api.module.ts | 32 + .../src/lib/mock-utils/db-for-test | 647 +++++++++++++++ .../src/lib/mock-utils/entities/addresses.ts | 69 ++ .../src/lib/mock-utils/entities/comments.ts | 57 ++ .../src/lib/mock-utils/entities/index.ts | 29 + .../src/lib/mock-utils/entities/notes.ts | 44 + .../src/lib/mock-utils/entities/pods.ts | 45 + .../entities/requests-have-pod-locks.ts | 91 +++ .../src/lib/mock-utils/entities/requests.ts | 48 ++ .../src/lib/mock-utils/entities/roles.ts | 58 ++ .../lib/mock-utils/entities/user-groups.ts | 20 + .../src/lib/mock-utils/entities/users.ts | 133 +++ .../src/lib/mock-utils/index.ts | 94 +++ .../src/lib/mock-utils/utils/index.ts | 2 + .../lib/mock-utils/utils/provider-entities.ts | 69 ++ .../src/lib/mock-utils/utils/pull-data.ts | 122 +++ .../atomic-operation.module.ts | 71 ++ .../atomic-operation/constants/index.ts | 10 + .../atomic-operation/controllers/index.ts | 1 + .../controllers/operation.controller.spec.ts | 220 +++++ .../controllers/operation.controller.ts | 104 +++ .../factory/async-iterator.ts | 75 ++ .../modules/atomic-operation/factory/index.ts | 4 + .../factory/map-controller-entity.ts | 25 + .../factory/map-entity-name-to-entity.ts | 19 + .../factory/zod-input-operation.ts | 22 + .../src/lib/modules/atomic-operation/index.ts | 1 + .../pipes/input-operation.pipe.spec.ts | 75 ++ .../pipes/input-operation.pipe.ts | 32 + .../service/execute.service.spec.ts | 609 ++++++++++++++ .../service/execute.service.ts | 338 ++++++++ .../service/explorer.service.spec.ts | 97 +++ .../service/explorer.service.ts | 109 +++ .../modules/atomic-operation/service/index.ts | 3 + .../service/swagger.service.ts | 97 +++ .../modules/atomic-operation/types/index.ts | 33 + .../modules/atomic-operation/utils/index.ts | 1 + .../utils/zod/zod-helper.spec.ts | 591 ++++++++++++++ .../atomic-operation/utils/zod/zod-helper.ts | 176 ++++ .../json-api-nestjs/src/lib/modules/index.ts | 3 + .../src/lib/modules/micro-orm/index.ts | 2 + .../lib/modules/micro-orm/micro-orm.module.ts | 14 + .../src/lib/modules/micro-orm/type.ts | 3 + .../lib/modules/mixin/config/bindings.spec.ts | 9 + .../src/lib/modules/mixin/config/bindings.ts | 202 +++++ .../mixin/controller/json-base.controller.ts | 79 ++ .../src/lib/modules/mixin/decorators/index.ts | 2 + .../inject-service.decorator.spec.ts | 30 + .../inject-service.decorator.ts | 7 + .../json-api/json-api.decorator.spec.ts | 69 ++ .../decorators/json-api/json-api.decorator.ts | 19 + .../src/lib/modules/mixin/factory/index.ts | 1 + .../mixin/factory/zod-validate.factory.ts | 160 ++++ .../mixin/helper/bind-controller.spec.ts | 165 ++++ .../modules/mixin/helper/bind-controller.ts | 124 +++ .../mixin/helper/create-controller.spec.ts | 95 +++ .../modules/mixin/helper/create-controller.ts | 53 ++ .../src/lib/modules/mixin/helper/index.ts | 3 + .../lib/modules/mixin/helper/utils.spec.ts | 17 + .../src/lib/modules/mixin/helper/utils.ts | 41 + .../mixin/interceptors/error.interceptors.ts | 120 +++ .../lib/modules/mixin/interceptors/index.ts | 2 + .../interceptors/log-time.interceptors.ts | 30 + .../src/lib/modules/mixin/mixin.module.ts | 84 ++ .../check-item-entity.pipe.spec.ts | 55 ++ .../check-item-entity.pipe.ts | 34 + .../mixin/pipe/check-item-entity/index.ts | 1 + .../src/lib/modules/mixin/pipe/index.spec.ts | 46 ++ .../src/lib/modules/mixin/pipe/index.ts | 89 ++ .../pipe/parse-relationship-name/index.ts | 1 + .../parse-relationship-name.pipe.spec.ts | 58 ++ .../parse-relationship-name.pipe.ts | 38 + .../modules/mixin/pipe/patch-input/index.ts | 1 + .../pipe/patch-input/patch-input.pipe.spec.ts | 69 ++ .../pipe/patch-input/patch-input.pipe.ts | 33 + .../mixin/pipe/patch-relationship/index.ts | 1 + .../patch-relationship.pipe.spec.ts | 84 ++ .../patch-relationship.pipe.ts | 32 + .../modules/mixin/pipe/post-input/index.ts | 1 + .../pipe/post-input/post-input.pipe.spec.ts | 69 ++ .../mixin/pipe/post-input/post-input.pipe.ts | 32 + .../mixin/pipe/post-relationship/index.ts | 1 + .../post-relationship.pipe.spec.ts | 83 ++ .../post-relationship.pipe.ts | 32 + .../pipe/query-check-select-field/index.ts | 1 + .../query-check-select-field.spec.ts | 73 ++ .../query-check-select-field.ts | 25 + .../pipe/query-filed-on-include/index.ts | 1 + .../query-filed-in-include.pipe.spec.ts | 121 +++ .../query-filed-in-include.pipe.ts | 70 ++ .../modules/mixin/pipe/query-input/index.ts | 1 + .../pipe/query-input/query-input.pipe.spec.ts | 65 ++ .../pipe/query-input/query-input.pipe.ts | 33 + .../src/lib/modules/mixin/pipe/query/index.ts | 1 + .../mixin/pipe/query/query.pipe.spec.ts | 107 +++ .../modules/mixin/pipe/query/query.pipe.ts | 30 + .../lib/modules/mixin/types/binding.types.ts | 47 ++ .../mixin/types/decorator-options.types.ts | 11 + .../src/lib/modules/mixin/types/index.ts | 6 + .../lib/modules/mixin/types/module.types.ts | 28 + .../modules/mixin/types/orm-service.type.ts | 55 ++ .../src/lib/modules/mixin/types/utils.ts | 68 ++ .../src/lib/modules/mixin/types/zod-types.ts | 141 ++++ .../src/lib/modules/mixin/zod/index.ts | 6 + .../index.spec.ts | 42 + .../index.ts | 13 + .../zod/zod-input-patch-schema/index.spec.ts | 220 +++++ .../mixin/zod/zod-input-patch-schema/index.ts | 111 +++ .../index.spec.ts | 43 + .../index.ts | 13 + .../zod/zod-input-post-schema/index.spec.ts | 219 +++++ .../mixin/zod/zod-input-post-schema/index.ts | 109 +++ .../zod/zod-input-query-schema/fields.spec.ts | 111 +++ .../zod/zod-input-query-schema/fields.ts | 59 ++ .../zod/zod-input-query-schema/filter.spec.ts | 123 +++ .../zod/zod-input-query-schema/filter.ts | 196 +++++ .../zod-input-query-schema/include.spec.ts | 32 + .../zod/zod-input-query-schema/include.ts | 18 + .../zod/zod-input-query-schema/index.spec.ts | 136 ++++ .../mixin/zod/zod-input-query-schema/index.ts | 39 + .../zod/zod-input-query-schema/sort.spec.ts | 56 ++ .../mixin/zod/zod-input-query-schema/sort.ts | 37 + .../mixin/zod/zod-query-schema/fields.spec.ts | 160 ++++ .../mixin/zod/zod-query-schema/fields.ts | 53 ++ .../mixin/zod/zod-query-schema/filter.spec.ts | 381 +++++++++ .../mixin/zod/zod-query-schema/filter.ts | 243 ++++++ .../zod/zod-query-schema/include.spec.ts | 53 ++ .../mixin/zod/zod-query-schema/include.ts | 22 + .../mixin/zod/zod-query-schema/index.spec.ts | 238 ++++++ .../mixin/zod/zod-query-schema/index.ts | 99 +++ .../mixin/zod/zod-query-schema/sort.spec.ts | 126 +++ .../mixin/zod/zod-query-schema/sort.ts | 60 ++ .../mixin/zod/zod-share/attributes.spec.ts | 200 +++++ .../modules/mixin/zod/zod-share/attributes.ts | 202 +++++ .../modules/mixin/zod/zod-share/id.spec.ts | 37 + .../src/lib/modules/mixin/zod/zod-share/id.ts | 16 + .../lib/modules/mixin/zod/zod-share/index.ts | 6 + .../modules/mixin/zod/zod-share/page.spec.ts | 42 + .../lib/modules/mixin/zod/zod-share/page.ts | 23 + .../mixin/zod/zod-share/rel-data.spec.ts | 37 + .../modules/mixin/zod/zod-share/rel-data.ts | 32 + .../mixin/zod/zod-share/relationships.spec.ts | 270 ++++++ .../mixin/zod/zod-share/relationships.ts | 157 ++++ .../modules/mixin/zod/zod-share/type.spec.ts | 21 + .../lib/modules/mixin/zod/zod-share/type.ts | 8 + .../lib/modules/mixin/zod/zod-utils.spec.ts | 359 ++++++++ .../src/lib/modules/mixin/zod/zod-utils.ts | 69 ++ .../src/lib/modules/type-orm/constants/di.ts | 2 + .../lib/modules/type-orm/constants/index.ts | 4 + .../src/lib/modules/type-orm/factory/index.ts | 218 +++++ .../src/lib/modules/type-orm/index.ts | 2 + .../modules/type-orm/orm-helper/index.spec.ts | 283 +++++++ .../lib/modules/type-orm/orm-helper/index.ts | 294 +++++++ .../orm-methods/delete-one/delete-one.spec.ts | 71 ++ .../orm-methods/delete-one/delete-one.ts | 21 + .../delete-relationship.spec.ts | 190 +++++ .../delete-relationship.ts | 32 + .../orm-methods/get-all/get-all.spec.ts | 406 +++++++++ .../type-orm/orm-methods/get-all/get-all.ts | 264 ++++++ .../orm-methods/get-one/get-one.spec.ts | 191 +++++ .../type-orm/orm-methods/get-one/get-one.ts | 95 +++ .../get-relationship/get-relationship.spec.ts | 131 +++ .../get-relationship/get-relationship.ts | 71 ++ .../lib/modules/type-orm/orm-methods/index.ts | 21 + .../orm-methods/patch-one/patch-one.spec.ts | 308 +++++++ .../orm-methods/patch-one/patch-one.ts | 73 ++ .../patch-relationship.spec.ts | 230 ++++++ .../patch-relationship/patch-relationship.ts | 51 ++ .../orm-methods/post-one/post-one.spec.ts | 256 ++++++ .../type-orm/orm-methods/post-one/post-one.ts | 39 + .../post-relationship.spec.ts | 205 +++++ .../post-relationship/post-relationship.ts | 40 + .../service/entity-props-map.service.spec.ts | 85 ++ .../service/entity-props-map.service.ts | 102 +++ .../src/lib/modules/type-orm/service/index.ts | 4 + .../service/transform-data.service.spec.ts | 372 +++++++++ .../service/transform-data.service.ts | 259 ++++++ .../type-orm/service/type-orm.service.ts | 135 +++ .../service/typeorm-utils.service.spec.ts | 770 ++++++++++++++++++ .../type-orm/service/typeorm-utils.service.ts | 685 ++++++++++++++++ .../lib/modules/type-orm/type-orm.module.ts | 67 ++ .../src/lib/modules/type-orm/type.ts | 11 + .../src/lib/types/config-param.ts | 45 + .../src/lib/types/error.types.ts | 18 + .../json-api-nestjs/src/lib/types/index.ts | 5 + .../src/lib/types/module-common.types.ts | 29 + .../json-api-nestjs/src/lib/types/operand.ts | 26 + .../src/lib/types/util-types.ts | 24 + .../src/lib/utils/helper.spec.ts | 152 ++++ .../json-api-nestjs/src/lib/utils/helper.ts | 120 +++ .../json-api-nestjs/src/lib/utils/index.ts | 1 + migrations.json | 142 ++++ project.json | 14 + 222 files changed, 18878 insertions(+), 57 deletions(-) create mode 100644 .verdaccio/config.yml create mode 100644 docker-compose.yaml create mode 100644 libs/json-api/json-api-nestjs-shared/.eslintrc.json create mode 100644 libs/json-api/json-api-nestjs-shared/README.md create mode 100644 libs/json-api/json-api-nestjs-shared/jest.config.ts create mode 100644 libs/json-api/json-api-nestjs-shared/package.json create mode 100644 libs/json-api/json-api-nestjs-shared/project.json create mode 100644 libs/json-api/json-api-nestjs-shared/src/index.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts create mode 100644 libs/json-api/json-api-nestjs-shared/tsconfig.json create mode 100644 libs/json-api/json-api-nestjs-shared/tsconfig.lib.json create mode 100644 libs/json-api/json-api-nestjs-shared/tsconfig.spec.json create mode 100644 libs/json-api/json-api-nestjs/src/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/default.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/di.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/json-api.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/config-param.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/error.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/operand.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/util-types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/helper.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/index.ts create mode 100644 migrations.json create mode 100644 project.json diff --git a/.verdaccio/config.yml b/.verdaccio/config.yml new file mode 100644 index 00000000..f74420f2 --- /dev/null +++ b/.verdaccio/config.yml @@ -0,0 +1,28 @@ +# path to a directory with all packages +storage: ../tmp/local-registry/storage + +# a list of other known repositories we can talk to +uplinks: + npmjs: + url: https://registry.npmjs.org/ + maxage: 60m + +packages: + '**': + # give all users (including non-authenticated users) full access + # because it is a local registry + access: $all + publish: $all + unpublish: $all + + # if package is not available locally, proxy requests to npm registry + proxy: npmjs + +# log settings +log: + type: stdout + format: pretty + level: warn + +publish: + allow_offline: true # set offline to true to allow publish offline diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..52340133 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,51 @@ +version: '3.8' +services: +# api-gate: +# container_name: api-gate +# restart: always +# build: apps/api-gate/ +# ports: +# - "3000:3000" + postgres: + image: postgres:15.1-alpine + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + ports: + - '5432:5432' + volumes: + - db:/var/lib/postgresql/data + pgadmin: + container_name: 'pgadmin' + image: 'dpage/pgadmin4:latest' + hostname: pgadmin + depends_on: + - 'postgres' + environment: + - PGADMIN_DEFAULT_PASSWORD=password + - PGADMIN_DEFAULT_EMAIL=pg.admin@email.com + volumes: + - pgadmin:/root/.pgadmin + ports: + - '8000:80' +# redis: +# image: redis:6.2-alpine +# restart: always +# ports: +# - '6379:6379' +# command: redis-server --save 20 1 --loglevel warning +# volumes: +# - redis:/data +# jaeger: +# image: jaegertracing/all-in-one:1.41 +# ports: +# - "16686:16686" +# - "14268:14268" +volumes: + db: + driver: local +# redis: +# driver: local + pgadmin: + driver: local diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts index 0584fd3c..b85cd65f 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts @@ -338,7 +338,7 @@ export class JsonApiUtilsService { } return { ...acum, - [key]: data, + [key]: { data }, }; }, {} as Relationships); diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts index 1450e19d..c88c995a 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts @@ -1,4 +1,4 @@ -import { QueryField } from 'json-shared-type'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; import { EntityProps, EntityRelation } from './entity'; import { TypeOfArray } from './utils'; import { Operands, OperandsRelation } from './filter-operand'; diff --git a/libs/json-api/json-api-nestjs-shared/.eslintrc.json b/libs/json-api/json-api-nestjs-shared/.eslintrc.json new file mode 100644 index 00000000..0af28030 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"] + } + ] + } + } + ] +} diff --git a/libs/json-api/json-api-nestjs-shared/README.md b/libs/json-api/json-api-nestjs-shared/README.md new file mode 100644 index 00000000..8703f1bf --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/README.md @@ -0,0 +1,11 @@ +# json-api-nestjs-shared + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build json-api-nestjs-shared` to build the library. + +## Running unit tests + +Run `nx test json-api-nestjs-shared` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/json-api/json-api-nestjs-shared/jest.config.ts b/libs/json-api/json-api-nestjs-shared/jest.config.ts new file mode 100644 index 00000000..67ade3e9 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/jest.config.ts @@ -0,0 +1,10 @@ +export default { + displayName: 'json-api-nestjs-shared', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/json-api/json-api-nestjs-shared', +}; diff --git a/libs/json-api/json-api-nestjs-shared/package.json b/libs/json-api/json-api-nestjs-shared/package.json new file mode 100644 index 00000000..25918d9b --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/package.json @@ -0,0 +1,10 @@ +{ + "name": "@klerick/json-api-nestjs-shared", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts" +} diff --git a/libs/json-api/json-api-nestjs-shared/project.json b/libs/json-api/json-api-nestjs-shared/project.json new file mode 100644 index 00000000..cdb32619 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/project.json @@ -0,0 +1,33 @@ +{ + "name": "json-api-nestjs-shared", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/json-api/json-api-nestjs-shared/src", + "projectType": "library", + "release": { + "version": { + "generatorOptions": { + "packageRoot": "dist/{projectRoot}", + "currentVersionResolver": "git-tag" + } + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/json-api/json-api-nestjs-shared", + "tsConfig": "libs/json-api/json-api-nestjs-shared/tsconfig.lib.json", + "packageJson": "libs/json-api/json-api-nestjs-shared/package.json", + "main": "libs/json-api/json-api-nestjs-shared/src/index.ts", + "assets": ["libs/json-api/json-api-nestjs-shared/*.md"] + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + } + } +} diff --git a/libs/json-api/json-api-nestjs-shared/src/index.ts b/libs/json-api/json-api-nestjs-shared/src/index.ts new file mode 100644 index 00000000..a0fe9b9f --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/utils'; +export * from './lib/types'; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts new file mode 100644 index 00000000..5fbdd04f --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts @@ -0,0 +1,17 @@ +export type EntityField = + | string + | number + | bigint + | boolean + | string[] + | number[] + | null + | Date; + +export type EntityProps = { + [P in keyof T]: T[P] extends EntityField ? P : never; +}[keyof T]; + +export type EntityRelation = { + [P in keyof T]: T[P] extends EntityField ? never : P; +}[keyof T]; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts new file mode 100644 index 00000000..33b70ba6 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts @@ -0,0 +1,4 @@ +export * from './utils-string.type'; +export * from './query-type'; +export * from './entity-type'; +export * from './response-body'; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts new file mode 100644 index 00000000..ddeead7a --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts @@ -0,0 +1,36 @@ +export enum QueryField { + filter = 'filter', + sort = 'sort', + include = 'include', + page = 'page', + fields = 'fields', +} + +export enum FilterOperand { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', + in = 'in', + nin = 'nin', + some = 'some', +} + +export enum FilterOperandOnlyInNin { + in = 'in', + nin = 'nin', +} +export enum FilterOperandOnlySimple { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', +} diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts new file mode 100644 index 00000000..fbe91dde --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts @@ -0,0 +1,69 @@ +import { + EntityField, + EntityProps, + EntityRelation, + TypeOfArray, + ValueOf, +} from '.'; + +export type PageProps = { + totalItems: number; + pageNumber: number; + pageSize: number; +}; + +export type DebugMetaProps = Partial<{ + time: number; +}>; + +export type MainData = { + type: T; + id: string; +}; + +export type Links = { + self: string; + related?: string; +}; + +export type Attributes = { + [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; +}; + +export type Data = { + data?: E extends unknown[] ? MainData[] : MainData | null; +}; + +export type Relationships = { + [P in EntityRelation]?: { + links: Links; + } & Data; +}; + +export type Include = ValueOf<{ + [P in EntityRelation]: ResourceData>; +}>; + +export type ResourceData = MainData & { + attributes?: Attributes; + relationships?: Relationships; + links: Omit; +}; + +export type MetaProps = R extends null ? T : T & R; + +export type ResourceObject< + T, + R extends 'object' | 'array' = 'object', + M = null +> = { + meta: R extends 'array' + ? MetaProps + : MetaProps; + data: R extends 'array' ? ResourceData[] : ResourceData; + included?: Include[]; +}; + +export type ResourceObjectRelationships> = { + meta: DebugMetaProps; +} & Required>; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts new file mode 100644 index 00000000..2d4284b2 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts @@ -0,0 +1,20 @@ +export type KebabCase = S extends `${infer C}${infer T}` + ? KebabCase extends infer U + ? U extends string + ? T extends Uncapitalize + ? `${Uncapitalize}${U}` + : `${Uncapitalize}-${U}` + : never + : never + : S; + +export type KebabToCamelCase = + S extends `${infer T}-${infer U}-${infer V}` + ? `${T}${Capitalize}${Capitalize>}` + : S extends `${infer T}-${infer U}` + ? `${Capitalize}${Capitalize>}` + : S; + +export type TypeOfArray = T extends (infer U)[] ? U : T; + +export type ValueOf = T[keyof T]; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts new file mode 100644 index 00000000..a7799257 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts @@ -0,0 +1,2 @@ +export * from './string-utils'; +export * from './object-utils'; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts new file mode 100644 index 00000000..461455b3 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts @@ -0,0 +1,14 @@ +export const ObjectTyped = { + keys: Object.keys as (yourObject: T) => Array, + values: Object.values as (yourObject: U) => Array, + entries: Object.entries as ( + yourObject: O + ) => Array<[keyof O, O[keyof O]]>, + fromEntries: Object.fromEntries as ( + yourObjectEntries: [K, V][] + ) => Record, +}; + +export function isObject(item: unknown): item is object { + return typeof item === 'object' && !Array.isArray(item) && item !== null; +} diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts new file mode 100644 index 00000000..4596cd21 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts @@ -0,0 +1,42 @@ +import { + camelToKebab, + snakeToCamel, + isString, + kebabToCamel, +} from './string-utils'; + +describe('Test utils', () => { + it('camelToKebab', () => { + const result = camelToKebab('ApproverGroups'); + const result1 = camelToKebab('Users'); + + expect(result).toBe('approver-groups'); + expect(result1).toBe('users'); + }); + + it('snakeToCamel', () => { + const result = snakeToCamel('test_test'); + const result1 = snakeToCamel('test-test'); + const result2 = snakeToCamel('testTest'); + const result3 = snakeToCamel('event_incident_typeFK'); + expect(result).toBe('testTest'); + expect(result1).toBe('testTest'); + expect(result2).toBe('testTest'); + expect(result3).toBe('eventIncidentTypeFK'); + }); + + it('isString', () => { + expect(isString('string')).toBe(true); + expect(isString(String('string'))).toBe(true); + expect(isString(new Date())).toBe(false); + expect(isString(class {})).toBe(false); + }); + + it('kebabToCamel', () => { + const type = 'users-group'; + const type1 = 'users'; + + expect(kebabToCamel(type)).toBe('UsersGroup'); + expect(kebabToCamel(type1)).toBe('Users'); + }); +}); diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts new file mode 100644 index 00000000..3b678710 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts @@ -0,0 +1,38 @@ +import { KebabToCamelCase, KebabCase } from '../types'; + +export function isString(value: T): value is P { + return typeof value === 'string' || value instanceof String; +} + +export function snakeToCamel(str: string): string { + if (!str.match(/[\s_-]/g)) { + return str; + } + return str.replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace('-', '').replace('_', '') + ); +} + +export function camelToKebab(string: S): KebabCase { + return string + .replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1') + .toLowerCase() as KebabCase; +} + +export function upperFirstLetter(string: S): Capitalize { + return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize; +} + +export function kebabToCamel(str: S): KebabToCamelCase { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join('') as KebabToCamelCase; +} + +export function capitalizeFirstChar(str: string) { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join(''); +} diff --git a/libs/json-api/json-api-nestjs-shared/tsconfig.json b/libs/json-api/json-api-nestjs-shared/tsconfig.json new file mode 100644 index 00000000..0dc79caa --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/json-api/json-api-nestjs-shared/tsconfig.lib.json b/libs/json-api/json-api-nestjs-shared/tsconfig.lib.json new file mode 100644 index 00000000..dbf54fd7 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/json-api/json-api-nestjs-shared/tsconfig.spec.json b/libs/json-api/json-api-nestjs-shared/tsconfig.spec.json new file mode 100644 index 00000000..ab55b7c7 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/json-api/json-api-nestjs/.eslintrc.json b/libs/json-api/json-api-nestjs/.eslintrc.json index ac1a6002..0af28030 100644 --- a/libs/json-api/json-api-nestjs/.eslintrc.json +++ b/libs/json-api/json-api-nestjs/.eslintrc.json @@ -1,6 +1,6 @@ { - "extends": ["../../../.eslintrc.json"], - "ignorePatterns": ["!**/*", "**/*.spec.ts"], + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], @@ -18,7 +18,12 @@ "files": ["*.json"], "parser": "jsonc-eslint-parser", "rules": { - "@nx/dependency-checks": "error" + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"] + } + ] } } ] diff --git a/libs/json-api/json-api-nestjs/project.json b/libs/json-api/json-api-nestjs/project.json index f0cb4555..be561184 100644 --- a/libs/json-api/json-api-nestjs/project.json +++ b/libs/json-api/json-api-nestjs/project.json @@ -3,68 +3,31 @@ "$schema": "../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/json-api/json-api-nestjs/src", "projectType": "library", + "release": { + "version": { + "generatorOptions": { + "packageRoot": "dist/{projectRoot}", + "currentVersionResolver": "git-tag" + } + } + }, + "tags": [], "targets": { - "build-ts": { + "build": { "executor": "@nx/js:tsc", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/libs/json-api/json-api-nestjs", - "main": "libs/json-api/json-api-nestjs/src/index.ts", "tsConfig": "libs/json-api/json-api-nestjs/tsconfig.lib.json", - "assets": ["libs/json-api/json-api-nestjs/README.md"], - "external": "none", - "updateBuildableProjectDepsInPackageJson": true, - "buildableProjectDepsInPackageJsonType": "peerDependencies", - "generateExportsField": true - } - }, - "build": { - "executor": "nx:run-commands", - "dependsOn": [ - "build-ts" - ], - "options": { - "commands": ["rm -rf dist/libs/json-api/json-api-nestjs/libs"], - "cwd": "./", - "parallel": false - } - }, - "publish": { - "command": "node tools/scripts/publish.mjs json-api-nestjs {args.ver} {args.tag}", - "dependsOn": ["build"] - }, - "lint": { - "executor": "@nx/eslint:lint", - "outputs": ["{options.outputFile}"] - }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "options": { - "jestConfig": "libs/json-api/json-api-nestjs/jest.config.ts", - "codeCoverage": true, - "coverageReporters": ["json-summary"] - } - }, - "upload-badge": { - "executor": "nx:run-commands", - "dependsOn": [ - { - "target": "test" - } - ], - "options": { - "commands": ["node tools/scripts/upload-badge.mjs json-api-nestjs"], - "cwd": "./", - "parallel": false, - "outputPath": "{workspaceRoot}/libs/json-api/json-api-nestjs" + "packageJson": "libs/json-api/json-api-nestjs/package.json", + "main": "libs/json-api/json-api-nestjs/src/index.ts", + "assets": ["libs/json-api/json-api-nestjs/*.md"] } }, "nx-release-publish": { "options": { - "packageRoot": "dist/libs/json-api/json-api-nestjs" + "packageRoot": "dist/{projectRoot}" } } - }, - "tags": [] + } } diff --git a/libs/json-api/json-api-nestjs/src/index.ts b/libs/json-api/json-api-nestjs/src/index.ts new file mode 100644 index 00000000..e8d912cd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/index.ts @@ -0,0 +1,23 @@ +export { JsonApiModule } from './lib/json-api.module'; +export { TypeOrmModule, MicroOrmModule } from './lib/modules'; + +export { JsonApi, InjectService } from './lib/modules/mixin/decorators'; +export { OrmService as JsonApiService } from './lib/modules/mixin/types'; +export { JsonBaseController } from './lib/modules/mixin/controller/json-base.controller'; +export { + Query, + PatchData, + PostData, + PostRelationshipData, + PatchRelationshipData, +} from './lib/modules/mixin/zod'; + +export { + EntityRelation, + ResourceObject, + ResourceObjectRelationships, + QueryField, +} from '@klerick/json-api-nestjs-shared'; + +export { excludeMethod } from './lib/modules/mixin/config/bindings'; +export { entityForClass } from './lib/utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/default.ts b/libs/json-api/json-api-nestjs/src/lib/constants/default.ts new file mode 100644 index 00000000..de75ca27 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/default.ts @@ -0,0 +1,4 @@ +export const DEFAULT_CONNECTION_NAME = 'default'; + +export const DEFAULT_QUERY_PAGE = 1; +export const DEFAULT_PAGE_SIZE = 20; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/di.ts b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts new file mode 100644 index 00000000..ec21865e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts @@ -0,0 +1,28 @@ +export const CURRENT_ENTITY_MANAGER_TOKEN = Symbol( + 'CURRENT_ENTITY_MANAGER_TOKEN' +); +export const GLOBAL_MODULE_OPTIONS_TOKEN = Symbol('GLOBAL_MODULE_OPTIONS'); +export const ORM_SERVICE = Symbol('ORM_SERVICE'); +export const ORM_SERVICE_PROPS = Symbol('ORM_SERVICE_PROPS'); + +export const PARAMS_FOR_ZOD_SCHEMA = Symbol('PARAMS_FOR_ZOD_SCHEMA'); +export const FIELD_FOR_ENTITY = Symbol('FIELD_FOR_ENTITY'); +export const CONTROL_OPTIONS_TOKEN = Symbol('CONTROL_OPTIONS_TOKEN'); +export const RUN_IN_TRANSACTION_FUNCTION = Symbol( + 'RUN_IN_TRANSACTION_FUNCTION' +); + +export const CURRENT_ENTITY = Symbol('CURRENT_ENTITY'); +export const FIND_ONE_ROW_ENTITY = Symbol('FIND_ONE_ROW_ENTITY'); +export const CHECK_RELATION_NAME = Symbol('CHECK_RELATION_NAME'); + +export const ZOD_INPUT_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); +export const ZOD_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); +export const ZOD_POST_SCHEMA = Symbol('ZOD_POST_SCHEMA'); +export const ZOD_PATCH_SCHEMA = Symbol('ZOD_PATCH_SCHEMA'); +export const ZOD_POST_RELATIONSHIP_SCHEMA = Symbol( + 'ZOD_POST_RELATIONSHIP_SCHEMA' +); +export const ZOD_PATCH_RELATIONSHIP_SCHEMA = Symbol( + 'ZOD_PATCH_RELATIONSHIP_SCHEMA' +); diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/constants/index.ts new file mode 100644 index 00000000..83b9d181 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/index.ts @@ -0,0 +1,14 @@ +export * from './default'; +export * from './di'; +export * from './reflection'; + +export const JSON_API_CONTROLLER_POSTFIX = 'JsonApiController'; +export const JSON_API_MODULE_POSTFIX = 'JsonApiModule'; + +export const PARAMS_RESOURCE_ID = 'id'; +export const PARAMS_RELATION_ID = 'relId'; +export const PARAMS_RELATION_NAME = 'relName'; + +export const DESC = 'DESC'; +export const ASC = 'ASC'; +export const SORT_TYPE = [DESC, ASC] as const; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts b/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts new file mode 100644 index 00000000..6656d32f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts @@ -0,0 +1,2 @@ +export const JSON_API_DECORATOR_ENTITY = Symbol('JSON_API_ENTITY'); +export const JSON_API_DECORATOR_OPTIONS = Symbol('JSON_API_OPTIONS'); diff --git a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts new file mode 100644 index 00000000..d14b20fd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts @@ -0,0 +1,32 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { DiscoveryModule } from '@nestjs/core'; + +import { AnyEntity, EntityName, ModuleOptions } from './types'; +import { createMixinModule, prepareConfig, createAtomicModule } from './utils'; + +@Module({}) +export class JsonApiModule { + public static forRoot(options: ModuleOptions): DynamicModule { + const resultOption = prepareConfig(options); + + resultOption.imports.unshift(DiscoveryModule); + + const commonOrmModule = resultOption.type.forRoot(resultOption); + + const entitiesMixinModules = resultOption.entities.map( + (entity: EntityName) => + createMixinModule(entity, resultOption, commonOrmModule) + ); + + const operationModuleImport = createAtomicModule( + resultOption, + entitiesMixinModules, + commonOrmModule + ); + + return { + module: JsonApiModule, + imports: [...operationModuleImport, ...entitiesMixinModules], + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test b/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test new file mode 100644 index 00000000..fa08bc14 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test @@ -0,0 +1,647 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 12.5 +-- Dumped by pg_dump version 12.5 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +create extension "uuid-ossp"; + +-- +-- Name: comment_kind_enum; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.comment_kind_enum AS ENUM ( + 'COMMENT', + 'MESSAGE', + 'NOTE' +); + + +SET default_table_access_method = heap; + +-- +-- Name: addresses; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.addresses ( + id integer NOT NULL, + city character varying(70) DEFAULT NULL::character varying, + state character varying(70) DEFAULT NULL::character varying, + country character varying(70) DEFAULT NULL::character varying, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + array_field text[] +); + + +-- +-- Name: addresses_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.addresses_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: addresses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.addresses_id_seq OWNED BY public.addresses.id; + + +-- +-- Name: comments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.comments ( + id integer NOT NULL, + text text NOT NULL, + kind public.comment_kind_enum NOT NULL, + created_by integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: comments_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.comments_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.comments_id_seq OWNED BY public.comments.id; + + +-- +-- Name: notes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.notes ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + text text NOT NULL, + created_by integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + + +-- +-- Name: migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.migrations ( + id integer NOT NULL, + "timestamp" bigint NOT NULL, + name character varying NOT NULL +); + + +-- +-- Name: migrations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.migrations_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: migrations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.migrations_id_seq OWNED BY public.migrations.id; + + +-- +-- Name: pods; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.pods ( + id integer NOT NULL, + name character varying, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: pods_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.pods_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: pods_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.pods_id_seq OWNED BY public.pods.id; + + +-- +-- Name: requests; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.requests ( + id integer NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: requests_have_pod_locks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.requests_have_pod_locks ( + id integer NOT NULL, + request_id integer NOT NULL, + pod_id integer NOT NULL, + external_id integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: requests_have_pod_locks_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.requests_have_pod_locks_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: requests_have_pod_locks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.requests_have_pod_locks_id_seq OWNED BY public.requests_have_pod_locks.id; + + +-- +-- Name: requests_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.requests_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: requests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.requests_id_seq OWNED BY public.requests.id; + + +-- +-- Name: roles; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.roles ( + id integer NOT NULL, + name character varying(128) DEFAULT NULL::character varying, + key character varying(128) NOT NULL, + is_default boolean DEFAULT false, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.roles_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.roles_id_seq OWNED BY public.roles.id; + + +-- +-- Name: typeorm_metadata; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.typeorm_metadata ( + type character varying NOT NULL, + database character varying, + schema character varying, + "table" character varying, + name character varying, + value text +); + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users ( + id integer NOT NULL, + login character varying(100) NOT NULL, + first_name character varying, + last_name character varying, + is_active boolean DEFAULT false, + test_real real[], + test_array_null real[], + manager_id integer, + addresses_id integer, + user_groups_id integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + test_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + +-- +-- Name: user_groups; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_groups ( + id integer NOT NULL, + label character varying NOT NULL +); + + +-- +-- Name: users_have_roles; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users_have_roles ( + id integer NOT NULL, + user_id integer NOT NULL, + role_id integer NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: users_have_roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.users_have_roles_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: users_have_roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.users_have_roles_id_seq OWNED BY public.users_have_roles.id; + + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: user_groups_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_groups_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- +-- Name: user_groups_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_groups_id_seq OWNED BY public.user_groups.id; + + +-- +-- Name: addresses id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.addresses ALTER COLUMN id SET DEFAULT nextval('public.addresses_id_seq'::regclass); + + +-- +-- Name: comments id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments ALTER COLUMN id SET DEFAULT nextval('public.comments_id_seq'::regclass); + + +-- +-- Name: migrations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.migrations ALTER COLUMN id SET DEFAULT nextval('public.migrations_id_seq'::regclass); + + +-- +-- Name: pods id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pods ALTER COLUMN id SET DEFAULT nextval('public.pods_id_seq'::regclass); + + +-- +-- Name: requests id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests ALTER COLUMN id SET DEFAULT nextval('public.requests_id_seq'::regclass); + + +-- +-- Name: requests_have_pod_locks id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests_have_pod_locks ALTER COLUMN id SET DEFAULT nextval('public.requests_have_pod_locks_id_seq'::regclass); + + +-- +-- Name: roles id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.roles ALTER COLUMN id SET DEFAULT nextval('public.roles_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_groups ALTER COLUMN id SET DEFAULT nextval('public.user_groups_id_seq'::regclass); + + +-- +-- Name: users_have_roles id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_have_roles ALTER COLUMN id SET DEFAULT nextval('public.users_have_roles_id_seq'::regclass); + + +-- +-- Name: requests PK_0428f484e96f9e6a55955f29b5f; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests + ADD CONSTRAINT "PK_0428f484e96f9e6a55955f29b5f" PRIMARY KEY (id); + + +-- +-- Name: addresses PK_745d8f43d3af10ab8247465e450; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.addresses + ADD CONSTRAINT "PK_745d8f43d3af10ab8247465e450" PRIMARY KEY (id); + + +-- +-- Name: comments PK_8bf68bc960f2b69e818bdb90dcb; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments + ADD CONSTRAINT "PK_8bf68bc960f2b69e818bdb90dcb" PRIMARY KEY (id); + + +-- +-- Name: migrations PK_8c82d7f526340ab734260ea46be; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.migrations + ADD CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id); + + +-- +-- Name: users_have_roles PK_9bb88c2f9f64bff7570e4108108; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_have_roles + ADD CONSTRAINT "PK_9bb88c2f9f64bff7570e4108108" PRIMARY KEY (id); + + +-- +-- Name: users PK_a3ffb1c0c8416b9fc6f907b7433; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY (id); + + +-- +-- Name: users PK_user_groups; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_groups + ADD CONSTRAINT "PK_user_groups" PRIMARY KEY (id); + +-- +-- Name: pods PK_b00bbc2c7fb41627be2b169f0dd; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pods + ADD CONSTRAINT "PK_b00bbc2c7fb41627be2b169f0dd" PRIMARY KEY (id); + + +-- +-- Name: roles PK_c1433d71a4838793a49dcad46ab; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.roles + ADD CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY (id); + + +-- +-- Name: requests_have_pod_locks PK_f214657396a396b70a697b04a85; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests_have_pod_locks + ADD CONSTRAINT "PK_f214657396a396b70a697b04a85" PRIMARY KEY (id); + + +-- +-- Name: users UQ_2d443082eccd5198f95f2a36e2c; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT "UQ_2d443082eccd5198f95f2a36e2c" UNIQUE (login); + + +-- +-- Name: roles UQ_a87cf0659c3ac379b339acf36a2; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.roles + ADD CONSTRAINT "UQ_a87cf0659c3ac379b339acf36a2" UNIQUE (key); + + +-- +-- Name: IDX_48d6a9a1ab3943e6c6d2a25d2e; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX "IDX_48d6a9a1ab3943e6c6d2a25d2e" ON public.requests_have_pod_locks USING btree (request_id, pod_id); + + +-- +-- Name: IDX_61c360686dfe8d62a9b03873bf; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX "IDX_61c360686dfe8d62a9b03873bf" ON public.users_have_roles USING btree (user_id, role_id); + + +-- +-- Name: users FK_2f8d527df0d3acb8aa51945a968; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT "FK_2f8d527df0d3acb8aa51945a968" FOREIGN KEY (addresses_id) REFERENCES public.addresses(id); + + +-- +-- Name: users FK_user_groups; Type: FK CONSTRAINT; Schema: public; Owner: - +-- +ALTER TABLE ONLY public.users + ADD CONSTRAINT "FK_user_groups" FOREIGN KEY (user_groups_id) REFERENCES public.user_groups(id); + + +-- +-- Name: users_have_roles FK_6e768e03083247102b401b74b46; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_have_roles + ADD CONSTRAINT "FK_6e768e03083247102b401b74b46" FOREIGN KEY (role_id) REFERENCES public.roles(id); + + +-- +-- Name: comments FK_980bfefe00ed11685f325d0bd4c; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments + ADD CONSTRAINT "FK_980bfefe00ed11685f325d0bd4c" FOREIGN KEY (created_by) REFERENCES public.users(id); + + +-- +-- Name: notes FK_980bfefe00ed11685f325d0bd4c; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notes + ADD CONSTRAINT "FK_notes" FOREIGN KEY (created_by) REFERENCES public.users(id); + + +-- +-- Name: requests_have_pod_locks FK_c7531fe6bbb926bba3f69fcbb55; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests_have_pod_locks + ADD CONSTRAINT "FK_c7531fe6bbb926bba3f69fcbb55" FOREIGN KEY (pod_id) REFERENCES public.pods(id); + + +-- +-- Name: users_have_roles FK_df6a0246fcd887dd8ffeed2c292; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_have_roles + ADD CONSTRAINT "FK_df6a0246fcd887dd8ffeed2c292" FOREIGN KEY (user_id) REFERENCES public.users(id); + + +-- +-- Name: requests_have_pod_locks FK_f3729b493fcdb7309cad08837ff; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests_have_pod_locks + ADD CONSTRAINT "FK_f3729b493fcdb7309cad08837ff" FOREIGN KEY (request_id) REFERENCES public.requests(id); + + +-- +-- Name: users FK_fba2d8e029689aa8fea98e53c91; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT "FK_fba2d8e029689aa8fea98e53c91" FOREIGN KEY (manager_id) REFERENCES public.users(id); + + +-- +-- PostgreSQL database dump complete +-- diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts new file mode 100644 index 00000000..3f0c7dab --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts @@ -0,0 +1,69 @@ +import { + PrimaryGeneratedColumn, + OneToOne, + Column, + Entity, + UpdateDateColumn, +} from 'typeorm'; + +import { Users, IUsers } from './index'; + +export type IAddresses = Addresses; + +@Entity('addresses') +export class Addresses { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'varchar', + length: 70, + nullable: true, + default: 'NULL', + }) + public city!: string; + + @Column({ + type: 'varchar', + length: 70, + nullable: true, + default: 'NULL', + }) + public state!: string; + + @Column({ + type: 'varchar', + length: 68, + nullable: true, + default: 'NULL', + }) + public country!: string; + + @Column({ + name: 'array_field', + type: 'varchar', + nullable: true, + default: 'NULL', + array: true, + }) + public arrayField!: string[]; + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @OneToOne(() => Users, (item) => item.addresses) + public user!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts new file mode 100644 index 00000000..c6f3ff8e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts @@ -0,0 +1,57 @@ +import { + PrimaryGeneratedColumn, + Column, + Entity, + JoinColumn, + ManyToOne, + UpdateDateColumn, +} from 'typeorm'; + +export enum CommentKind { + Comment = 'COMMENT', + Message = 'MESSAGE', + Note = 'NOTE', +} + +import { Users, IUsers } from './index'; + +@Entity('comments') +export class Comments { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'text', + nullable: false, + }) + public text!: string; + + @Column({ + type: 'enum', + enum: CommentKind, + nullable: false, + }) + public kind!: CommentKind; + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToOne(() => Users, (item) => item.id) + @JoinColumn({ + name: 'created_by', + }) + public createdBy!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts new file mode 100644 index 00000000..ba42e083 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts @@ -0,0 +1,29 @@ +export * from './users'; +export * from './roles'; +export * from './requests-have-pod-locks'; +export * from './requests'; +export * from './pods'; +export * from './comments'; +export * from './addresses'; +export * from './user-groups'; +export * from './notes'; + +import { Users } from './users'; +import { Roles } from './roles'; +import { Requests } from './requests'; +import { Pods } from './pods'; +import { Comments } from './comments'; +import { Addresses } from './addresses'; +import { UserGroups } from './user-groups'; +import { Notes } from './notes'; + +export const Entities = [ + Users, + Roles, + Requests, + Pods, + Comments, + Addresses, + UserGroups, + Notes, +]; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts new file mode 100644 index 00000000..e8694aca --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts @@ -0,0 +1,44 @@ +import { + PrimaryGeneratedColumn, + Column, + Entity, + JoinColumn, + ManyToOne, + UpdateDateColumn, +} from 'typeorm'; + +import { Users, IUsers } from './index'; + +@Entity('notes') +export class Notes { + @PrimaryGeneratedColumn('uuid') + public id!: string; + + @Column({ + type: 'text', + nullable: false, + }) + public text!: string; + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToOne(() => Users, (item) => item.notes) + @JoinColumn({ + name: 'created_by', + }) + public createdBy!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts new file mode 100644 index 00000000..b5fb898f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts @@ -0,0 +1,45 @@ +import { + Column, + CreateDateColumn, + Entity, + ManyToMany, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; + +import { IRequests, Requests } from './index'; + +export type IPods = Pods; + +@Entity('pods') +export class Pods { + @PrimaryColumn() + public id!: string; + + @Column({ + type: 'varchar', + length: 50, + nullable: false, + unique: true, + }) + public name!: string; + + @CreateDateColumn({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToMany(() => Requests, (item) => item.podLocks) + public lockedRequests!: IRequests[]; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts new file mode 100644 index 00000000..7f1f8125 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts @@ -0,0 +1,91 @@ +import { + AfterLoad, + BeforeInsert, + BeforeRemove, + BeforeUpdate, + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +export type IRequestsHavePodLocks = RequestsHavePodLocks; + +@Entity('requests_have_pod_locks') +export class RequestsHavePodLocks { + @PrimaryGeneratedColumn() + public id!: number; + + @AfterLoad() + protected getRequestId() { + this.requestId = this.request_id; + } + + @BeforeInsert() + @BeforeUpdate() + @BeforeRemove() + protected setRequestId() { + if (this.requestId) { + this.request_id = this.requestId; + } + } + + public requestId!: number; + + @AfterLoad() + protected getPodId() { + this.podId = this.pod_id; + } + + @BeforeInsert() + @BeforeUpdate() + @BeforeRemove() + protected setPodId() { + if (this.podId) { + this.pod_id = this.podId; + } + } + + public podId!: number; + + @Column({ + name: 'request_id', + type: 'int', + nullable: false, + }) + protected request_id!: number; + + @Column({ + name: 'pod_id', + type: 'int', + nullable: false, + }) + protected pod_id!: number; + + @CreateDateColumn({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @Column({ + name: 'external_id', + type: 'int', + nullable: true, + unsigned: true, + default: 'NULL', + unique: true, + }) + public externalId!: number; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts new file mode 100644 index 00000000..9bd64266 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts @@ -0,0 +1,48 @@ +import { + PrimaryGeneratedColumn, + Entity, + CreateDateColumn, + UpdateDateColumn, + ManyToMany, + JoinTable, +} from 'typeorm'; + +import { Pods, IPods } from './index'; + +export type IRequests = Requests; + +@Entity('requests') +export class Requests { + @PrimaryGeneratedColumn() + public id!: number; + + @CreateDateColumn({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToMany(() => Pods, (item) => item.lockedRequests) + @JoinTable({ + name: 'requests_have_pod_locks', + inverseJoinColumn: { + referencedColumnName: 'id', + name: 'pod_id', + }, + joinColumn: { + referencedColumnName: 'id', + name: 'request_id', + }, + }) + public podLocks!: IPods[]; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts new file mode 100644 index 00000000..4689628a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts @@ -0,0 +1,58 @@ +import { + PrimaryGeneratedColumn, + Entity, + Column, + ManyToMany, + UpdateDateColumn, +} from 'typeorm'; + +import { Users, IUsers } from './index'; + +@Entity('roles') +export class Roles { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'varchar', + length: 128, + nullable: true, + default: 'NULL', + }) + public name!: string; + + @Column({ + type: 'varchar', + length: 128, + nullable: false, + unique: true, + }) + public key!: string; + + @Column({ + name: 'is_default', + type: 'boolean', + default: 'false', + }) + public isDefault!: boolean; + + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToMany(() => Users, (item) => item.roles) + public users!: IUsers[]; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts new file mode 100644 index 00000000..a6727416 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts @@ -0,0 +1,20 @@ +import { PrimaryGeneratedColumn, OneToMany, Entity, Column } from 'typeorm'; + +import { IUsers, Users } from './index'; + +@Entity('user_groups') +export class UserGroups { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'varchar', + length: 50, + nullable: false, + unique: true, + }) + public label!: string; + + @OneToMany(() => Users, (item) => item.userGroup) + public users!: IUsers[]; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts new file mode 100644 index 00000000..bdf61878 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts @@ -0,0 +1,133 @@ +import { + PrimaryGeneratedColumn, + ManyToMany, + JoinColumn, + JoinTable, + OneToOne, + OneToMany, + Entity, + Column, + UpdateDateColumn, + ManyToOne, +} from 'typeorm'; + +import { Addresses, Roles, Comments, Notes, UserGroups } from './index'; + +export type IUsers = Users; + +@Entity('users') +export class Users { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'varchar', + length: 100, + nullable: false, + unique: true, + }) + public login!: string; + + @Column({ + name: 'first_name', + type: 'varchar', + length: 100, + nullable: true, + default: 'NULL', + }) + public firstName!: string; + + @Column({ + name: 'test_real', + type: 'real', + array: true, + default: [], + }) + public testReal!: number[]; + + @Column({ + name: 'test_array_null', + type: 'real', + array: true, + nullable: true, + }) + public testArrayNull!: number[] | null; + + @Column({ + name: 'last_name', + type: 'varchar', + length: 100, + nullable: true, + default: 'NULL', + }) + public lastName!: string; + + @Column({ + name: 'is_active', + type: 'boolean', + width: 1, + nullable: true, + default: false, + }) + public isActive!: boolean; + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @Column({ + name: 'test_date', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public testDate!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @OneToOne(() => Addresses, (item) => item.id) + @JoinColumn({ + name: 'addresses_id', + }) + public addresses!: Addresses; + + @OneToOne(() => Users, (item) => item.id) + @JoinColumn({ + name: 'manager_id', + }) + public manager!: Users; + + @ManyToMany(() => Roles, (item) => item.users) + @JoinTable({ + name: 'users_have_roles', + inverseJoinColumn: { + referencedColumnName: 'id', + name: 'role_id', + }, + joinColumn: { + referencedColumnName: 'id', + name: 'user_id', + }, + }) + public roles!: Roles[]; + + @OneToMany(() => Comments, (item) => item.createdBy) + public comments!: Comments[]; + + @OneToMany(() => Notes, (item) => item.createdBy) + public notes!: Notes[]; + + @ManyToOne(() => UserGroups, (userGroup) => userGroup.id) + @JoinColumn({ name: 'user_groups_id' }) + public userGroup!: UserGroups; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts new file mode 100644 index 00000000..bd6b43f6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts @@ -0,0 +1,94 @@ +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DynamicModule } from '@nestjs/common'; +import { DataType, IMemoryDb, newDb } from 'pg-mem'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { + Users, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + UserGroups, + Notes, +} from './entities'; +import { DataSource } from 'typeorm'; + +import { v4 } from 'uuid'; + +export * from './entities'; +export * from './utils'; + +export const entities = [ + Users, + UserGroups, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + Notes, +]; + +export function createAndPullSchemaBase(): IMemoryDb { + const dump = readFileSync(join(__dirname, 'db-for-test'), { + encoding: 'utf8', + }); + const db = newDb({ + autoCreateForeignKeyIndices: true, + }); + + db.public.registerFunction({ + name: 'current_database', + implementation: () => 'test', + }); + + db.public.registerFunction({ + name: 'version', + implementation: () => + 'PostgreSQL 12.5 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.2.1_pre1) 10.2.1 20201203, 64-bit', + }); + + db.registerExtension('uuid-ossp', (schema) => { + schema.registerFunction({ + name: 'uuid_generate_v4', + returns: DataType.uuid, + implementation: v4, + impure: true, + }); + }); + db.public.none(dump); + return db; +} +export function mockDBTestModule(db: IMemoryDb): DynamicModule { + return TypeOrmModule.forRootAsync({ + useFactory() { + return { + type: 'postgres', + // logging: true, + entities: [ + Users, + UserGroups, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + Notes, + ], + }; + }, + async dataSourceFactory(options) { + const dataSource: DataSource = await db.adapters.createTypeormDataSource( + options + ); + + return dataSource; + }, + }); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts new file mode 100644 index 00000000..7cb5aa8b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts @@ -0,0 +1,2 @@ +export * from './pull-data'; +export * from './provider-entities'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts new file mode 100644 index 00000000..4b47577b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts @@ -0,0 +1,69 @@ +import { DataSource, Repository } from 'typeorm'; +import { Provider } from '@nestjs/common'; +import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm'; + +import { + Addresses, + Comments, + Entities, + Notes, + Pods, + Roles, + UserGroups, +} from '../entities'; +import { Users } from '../entities'; +import { DEFAULT_CONNECTION_NAME } from '../../constants'; +import { TestingModule } from '@nestjs/testing'; + +export function providerEntities( + dataSourceToken: ReturnType +): Provider[] { + return Entities.map((entitiy) => { + return { + provide: getRepositoryToken(entitiy, DEFAULT_CONNECTION_NAME), + useFactory(dataSource: DataSource) { + return dataSource.getRepository(entitiy); + }, + inject: [getDataSourceToken()], + }; + }); +} + +export function getRepository(module: TestingModule) { + const userRepository = module.get>( + getRepositoryToken(Users, DEFAULT_CONNECTION_NAME) + ); + + const addressesRepository = module.get>( + getRepositoryToken(Addresses, DEFAULT_CONNECTION_NAME) + ); + + const notesRepository = module.get>( + getRepositoryToken(Notes, DEFAULT_CONNECTION_NAME) + ); + + const commentsRepository = module.get>( + getRepositoryToken(Comments, DEFAULT_CONNECTION_NAME) + ); + const rolesRepository = module.get>( + getRepositoryToken(Roles, DEFAULT_CONNECTION_NAME) + ); + + const userGroupRepository = module.get>( + getRepositoryToken(UserGroups, DEFAULT_CONNECTION_NAME) + ); + + const podsRepository = module.get>( + getRepositoryToken(Pods, DEFAULT_CONNECTION_NAME) + ); + + return { + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + podsRepository, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts new file mode 100644 index 00000000..f28c693e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts @@ -0,0 +1,122 @@ +import { Repository } from 'typeorm'; +import { faker } from '@faker-js/faker'; +import { + Addresses, + CommentKind, + Comments, + Notes, + Roles, + UserGroups, + Users, +} from '../entities'; + +export async function pullAddress(addressRepo: Repository) { + const address = new Addresses(); + address.city = faker.location.city(); + address.country = faker.location.country(); + address.arrayField = [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ]; + address.state = faker.location.state(); + return addressRepo.save(address); +} + +export async function pullComment(commentRepo: Repository) { + const comment = new Comments(); + comment.text = faker.lorem.paragraph(faker.number.int(5)); + comment.kind = CommentKind.Comment; + return commentRepo.save(comment); +} + +export async function pullNote(noteRepo: Repository) { + const note = new Notes(); + note.text = faker.lorem.paragraph(faker.number.int(5)); + return noteRepo.save(note); +} + +export async function pullRole(roleRepo: Repository) { + const role = new Roles(); + role.key = faker.string.alphanumeric(5); + role.name = faker.string.alphanumeric(5); + return roleRepo.save(role); +} + +export async function pullUser(userPero: Repository) { + const user = new Users(); + user.firstName = faker.person.firstName(); + user.lastName = faker.person.lastName(); + user.isActive = faker.datatype.boolean(); + user.login = faker.internet.userName({ + lastName: user.lastName, + firstName: user.firstName, + }); + user.testReal = [faker.number.float({ fractionDigits: 4 })]; + user.testArrayNull = null; + + user.testDate = faker.date.anytime(); + + return userPero.save(user); +} + +export async function pullUserGroup(userGroupRepo: Repository) { + const userGroup = new UserGroups(); + userGroup.label = faker.string.alphanumeric(5); + return userGroupRepo.save(userGroup); +} + +export async function pullAllData( + userPero: Repository, + addressRepo?: Repository, + noteRepo?: Repository, + commentRepo?: Repository, + roleRepo?: Repository, + userGroupRepo?: Repository +) { + const user = await pullUser(userPero); + if (addressRepo) { + user.addresses = await pullAddress(addressRepo); + } + + if (noteRepo) { + user.notes = [ + await pullNote(noteRepo), + await pullNote(noteRepo), + await pullNote(noteRepo), + ]; + } + + if (commentRepo) { + user.comments = [ + await pullComment(commentRepo), + await pullComment(commentRepo), + await pullComment(commentRepo), + await pullComment(commentRepo), + ]; + } + + if (userGroupRepo) { + await pullUserGroup(userGroupRepo); + await pullUserGroup(userGroupRepo); + await pullUserGroup(userGroupRepo); + user.userGroup = await pullUserGroup(userGroupRepo); + } + + if (roleRepo) { + await pullRole(roleRepo); + await pullRole(roleRepo); + await pullRole(roleRepo); + user.roles = [ + await pullRole(roleRepo), + await pullRole(roleRepo), + await pullRole(roleRepo), + ]; + } + + user.manager = await pullUser(userPero); + await pullUser(userPero); + await pullUser(userPero); + await pullUser(userPero); + await userPero.save(user); + return user; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts new file mode 100644 index 00000000..46bf6797 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts @@ -0,0 +1,71 @@ +import { AsyncLocalStorage } from 'async_hooks'; +import { + DynamicModule, + Inject, + MiddlewareConsumer, + Module, + NestModule, +} from '@nestjs/common'; +import { DiscoveryModule } from '@nestjs/core'; + +import { OperationController } from './controllers'; +import { ExplorerService, ExecuteService, SwaggerService } from './service'; + +import { + MapControllerEntity, + MapEntityNameToEntity, + ZodInputOperation, + AsyncIterate, +} from './factory'; +import { ResultModuleOptions } from '../../types'; +import { MAP_CONTROLLER_INTERCEPTORS, OPTIONS } from './constants'; + +@Module({}) +export class AtomicOperationModule implements NestModule { + static forRoot( + options: ResultModuleOptions, + entityModules: DynamicModule[], + commonModule: DynamicModule + ): DynamicModule { + return { + module: AtomicOperationModule, + controllers: [OperationController], + providers: [ + ExplorerService, + ExecuteService, + SwaggerService, + AsyncIterate, + MapControllerEntity(options.entities, entityModules), + MapEntityNameToEntity(options.entities), + ZodInputOperation(), + { + provide: MAP_CONTROLLER_INTERCEPTORS, + useValue: new Map(), + }, + { + provide: OPTIONS, + useValue: options.options, + }, + { + provide: AsyncLocalStorage, + useValue: new AsyncLocalStorage(), + }, + ], + imports: [DiscoveryModule, commonModule], + }; + } + @Inject(AsyncLocalStorage) private readonly als!: AsyncLocalStorage; + + configure(consumer: MiddlewareConsumer) { + consumer + .apply((req: any, res: any, next: any) => { + const store = { + req: req, + res: res, + next: next, + }; + this.als.run(store, () => next()); + }) + .forRoutes('*'); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts new file mode 100644 index 00000000..ccace714 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts @@ -0,0 +1,10 @@ +export const MAP_CONTROLLER_ENTITY = Symbol('MAP_CONTROLLER_ENTITY'); +export const MAP_CONTROLLER_INTERCEPTORS = Symbol( + 'MAP_CONTROLLER_INTERCEPTORS' +); +export const MAP_ENTITY = Symbol('MAP_ENTITY'); +export const ZOD_INPUT_OPERATION = Symbol('ZOD_INPUT_OPERATION'); +export const ASYNC_ITERATOR_FACTORY = Symbol('ASYNC_ITERATOR_FACTORY'); +export const KEY_MAIN_INPUT_SCHEMA = 'atomic:operations'; +export const KEY_MAIN_OUTPUT_SCHEMA = 'atomic:results'; +export const OPTIONS = Symbol('OPTIONS'); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts new file mode 100644 index 00000000..e81188ae --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts @@ -0,0 +1 @@ +export * from './operation.controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts new file mode 100644 index 00000000..d080815b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts @@ -0,0 +1,220 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DiscoveryModule } from '@nestjs/core'; +import { HttpException } from '@nestjs/common'; +import { Module } from '@nestjs/core/injector/module'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { IMemoryDb } from 'pg-mem'; + +import { OperationController } from './operation.controller'; +import { ExecuteService, ExplorerService } from '../service'; +import { InputArray, Operation } from '../utils'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; +import { + createAndPullSchemaBase, + mockDBTestModule, + providerEntities, + Users, +} from '../../../mock-utils'; + +import { + ASYNC_ITERATOR_FACTORY, + KEY_MAIN_OUTPUT_SCHEMA, + MAP_CONTROLLER_ENTITY, + MAP_ENTITY, + ZOD_INPUT_OPERATION, + MAP_CONTROLLER_INTERCEPTORS, + OPTIONS, +} from '../constants'; + +import { OperationMethode } from '../types'; +import { AsyncLocalStorage } from 'async_hooks'; +import { CURRENT_DATA_SOURCE_TOKEN } from '../../type-orm/constants'; +import { ObjectLiteral } from '../../../types'; +import { RUN_IN_TRANSACTION_FUNCTION } from '../../../constants'; + +describe('OperationController', () => { + let db: IMemoryDb; + let operationController: OperationController; + let explorerService: ExplorerService; + let executeService: ExecuteService; + + beforeEach(async () => { + db = createAndPullSchemaBase(); + const app: TestingModule = await Test.createTestingModule({ + imports: [DiscoveryModule, mockDBTestModule(db)], + controllers: [OperationController], + providers: [ + ...providerEntities(getDataSourceToken()), + { + provide: CURRENT_DATA_SOURCE_TOKEN, + useValue: {}, + }, + ExplorerService, + ExecuteService, + { + provide: MAP_ENTITY, + useValue: {}, + }, + { + provide: RUN_IN_TRANSACTION_FUNCTION, + useValue: {}, + }, + { + provide: MAP_CONTROLLER_ENTITY, + useValue: {}, + }, + { + provide: ASYNC_ITERATOR_FACTORY, + useValue: {}, + }, + { + provide: ZOD_INPUT_OPERATION, + useValue: {}, + }, + { + provide: OPTIONS, + useValue: {}, + }, + { + provide: MAP_CONTROLLER_INTERCEPTORS, + useValue: {}, + }, + { + provide: AsyncLocalStorage, + useValue: new AsyncLocalStorage(), + }, + ], + }).compile(); + + operationController = app.get(OperationController); + explorerService = app.get>(ExplorerService); + executeService = app.get(ExecuteService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('index', () => { + it('should return the result of executeService.run', async () => { + const inputArrayMock: InputArray = [ + { + ref: { + id: '1', + relationship: 'belongs-to', + type: 'TypeA', + }, + op: Operation.add, + }, + ]; + const paramsForExecuteMock = [ + { + module: new (class Module {})() as Module, + params: [1, 'nameRel', { type: 'name', id: '' }], + methodName: 'patchOne', + controller: JsonBaseController, + }, + ]; + + const mockReturnData = { data: { someData: '' } }; + + const getControllerByEntityNameSpy = jest + .spyOn(explorerService, 'getControllerByEntityName') + .mockReturnValue(paramsForExecuteMock[0].controller); + const getMethodNameByParamSpy = jest + .spyOn(explorerService, 'getMethodNameByParam') + .mockReturnValue( + paramsForExecuteMock[0].methodName as OperationMethode + ); + const getModulesByControllerSpy = jest + .spyOn(explorerService, 'getParamsForMethod') + .mockReturnValue( + paramsForExecuteMock[0].params as Parameters< + JsonBaseController['deleteOne'] + > + ); + const getParamsForMethodSpy = jest + .spyOn(explorerService, 'getModulesByController') + .mockReturnValue(paramsForExecuteMock[0].module); + const runSpy = jest + .spyOn(executeService, 'run') + .mockResolvedValue([mockReturnData] as never); + + expect(await operationController.index(inputArrayMock)).toEqual({ + [KEY_MAIN_OUTPUT_SCHEMA]: [mockReturnData], + }); + + expect(getControllerByEntityNameSpy).toHaveBeenCalledWith('TypeA'); + expect(getMethodNameByParamSpy).toHaveBeenCalledWith( + inputArrayMock[0].op, + inputArrayMock[0].ref.id, + inputArrayMock[0].ref.relationship + ); + expect(getModulesByControllerSpy).toHaveBeenCalledWith( + paramsForExecuteMock[0].methodName, + { op: inputArrayMock[0].op, ref: inputArrayMock[0].ref } + ); + expect(getParamsForMethodSpy).toHaveBeenCalledWith( + paramsForExecuteMock[0].controller + ); + + expect(runSpy).toHaveBeenCalledWith(paramsForExecuteMock, []); + }); + + it('should throw NotFoundException when type does not exist', async () => { + const inputArrayMock: any[] = [ + { + ref: { + id: '1', + relationship: 'belongs-to', + type: 'TypeA', + }, + op: Operation.add, + }, + ]; + + jest + .spyOn(explorerService, 'getControllerByEntityName') + .mockImplementationOnce(() => { + throw new HttpException('Resource does not exist', 404); + }); + expect.assertions(1); + try { + await operationController.index(inputArrayMock as InputArray); + } catch (e) { + expect(e).toBeInstanceOf(HttpException); + } + }); + + it('should throw MethodNotAllowedException when operation not allowed', async () => { + const inputArrayMock = [ + { + ref: { + id: '1', + relationship: 'belongs-to', + type: 'TypeA', + }, + op: Operation.add, + }, + ]; + + jest + .spyOn(explorerService, 'getControllerByEntityName') + .mockReturnValue(Promise.resolve({}) as any); + + jest + .spyOn(explorerService, 'getMethodNameByParam') + .mockImplementationOnce(() => { + throw new HttpException('Operation not allowed', 405); + }); + + expect.assertions(1); + try { + await operationController.index(inputArrayMock as InputArray); + } catch (e) { + expect(e).toBeInstanceOf(HttpException); + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts new file mode 100644 index 00000000..7042e289 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts @@ -0,0 +1,104 @@ +import { + Body, + Controller, + Inject, + MethodNotAllowedException, + NotFoundException, + Post, + Type, +} from '@nestjs/common'; +import { Module } from '@nestjs/core/injector/module'; + +import { InputArray } from '../utils'; +import { InputOperationPipe } from '../pipes/input-operation.pipe'; +import { ExecuteService, ExplorerService } from '../service'; +import { KEY_MAIN_INPUT_SCHEMA, KEY_MAIN_OUTPUT_SCHEMA } from '../constants'; +import { OperationMethode, ParamsForExecute } from '../types'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; +import { ObjectLiteral as Entity, ValidateQueryError } from '../../../types'; + +@Controller('/') +export class OperationController { + @Inject(ExplorerService) private readonly explorerService!: ExplorerService; + @Inject(ExecuteService) private readonly executeService!: ExecuteService; + + @Post('') + async index(@Body(InputOperationPipe) inputOperationData: InputArray) { + const paramForCall: ParamsForExecute[] = []; + let i = 0; + for (const dataInput of inputOperationData) { + const { + ref: { relationship, id, type }, + op, + } = dataInput; + + let controller: Type>; + let methodName: OperationMethode; + let module: Module; + try { + controller = this.explorerService.getControllerByEntityName(type); + } catch (e) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${type}' does not exist`, + path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'ref', 'type'], + }; + throw new NotFoundException([error]); + } + try { + methodName = this.explorerService.getMethodNameByParam( + op, + id, + relationship + ); + } catch (e) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Operation '${op}' not allowed`, + path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'op'], + }; + throw new MethodNotAllowedException([error]); + } + + const params = this.explorerService.getParamsForMethod( + methodName, + dataInput + ); + + try { + module = this.explorerService.getModulesByController(controller); + } catch (e) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${type}' does not exist`, + path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'ref', 'type'], + }; + throw new NotFoundException([error]); + } + + paramForCall.push({ + controller, + methodName, + params, + module, + }); + + i++; + } + const tmpIds: (string | number)[] = []; + for (const item of inputOperationData) { + if (item.op !== 'add') continue; + if (!item.ref.tmpId) continue; + tmpIds.push(item.ref.tmpId); + } + + const result = await this.executeService.run(paramForCall, tmpIds); + + return { + [KEY_MAIN_OUTPUT_SCHEMA]: result.map((i) => ({ + data: i.data, + ...(i.meta ? { meta: i.meta } : {}), + })), + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts new file mode 100644 index 00000000..fd9f15b1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts @@ -0,0 +1,75 @@ +import { Provider } from '@nestjs/common'; +import { ASYNC_ITERATOR_FACTORY } from '../constants'; + +type ParamsInput = R extends (...arg: infer P) => any ? P : never; + +type ParamsReturn = R extends (...arg: any) => infer P + ? P extends Promise + ? T extends [infer K, ...any] + ? K + : T + : P + : never; + +export type IterateFactory< + R extends (...arg: any) => any = (...arg: any) => any +> = { + createIterator: ( + iterateObject: ParamsInput, + callback: R + ) => { + [Symbol.asyncIterator](): GeneralAsyncIterator< + R, + ParamsInput, + ParamsReturn + >; + }; +}; + +class GeneralAsyncIterator< + R extends (...arg: any[]) => any, + T = ParamsInput, + TReturn = ParamsReturn +> implements AsyncIterator +{ + private counter = 0; + private maxLimit!: number; + + constructor(private iterateObject: T[], private callback: R) { + if (!Array.isArray(iterateObject)) { + throw new Error('Expected iterateObject to be an array'); + } + this.maxLimit = iterateObject.length; + } + + async next(): Promise> { + const items = !Array.isArray(this.iterateObject[this.counter]) + ? [this.iterateObject[this.counter]] + : (this.iterateObject[this.counter] as T[]); + this.counter++; + + if (this.counter <= this.maxLimit) { + return this.callback(...items).then((r: TReturn) => ({ + done: false, + value: r, + })); + } else { + return Promise.resolve({ done: true, value: {} as TReturn }); + } + } +} + +export const AsyncIterate: Provider = { + provide: ASYNC_ITERATOR_FACTORY, + useFactory: () => ({ + createIterator any>( + iterateObject: ParamsInput, + callback: R + ) { + return { + [Symbol.asyncIterator]: () => + new GeneralAsyncIterator(iterateObject, callback), + }; + }, + }), +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts new file mode 100644 index 00000000..0dded8b9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts @@ -0,0 +1,4 @@ +export * from './zod-input-operation'; +export * from './map-controller-entity'; +export * from './map-entity-name-to-entity'; +export * from './async-iterator'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts new file mode 100644 index 00000000..64d23a17 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts @@ -0,0 +1,25 @@ +import { DynamicModule, ValueProvider } from '@nestjs/common'; +import { Type } from '@nestjs/common/interfaces/type.interface'; +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; +import { MapController } from '../types'; +import { MAP_CONTROLLER_ENTITY } from '../constants'; + +export function MapControllerEntity( + entities: EntityClassOrSchema[], + entityModules: DynamicModule[] +): ValueProvider { + const mapController = entities.reduce((acum, entity, index) => { + const entityModule = entityModules[index]; + if (entityModule.controllers) { + const controller = entityModule.controllers.at(0); + if (controller) acum.set(entity, controller); + } + + return acum; + }, new Map>()); + + return { + provide: MAP_CONTROLLER_ENTITY, + useValue: mapController, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts new file mode 100644 index 00000000..50a1207a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts @@ -0,0 +1,19 @@ +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; +import { ValueProvider } from '@nestjs/common'; +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; +import { MapEntity } from '../types'; +import { MAP_ENTITY } from '../constants'; +import { getEntityName } from '../../mixin/helper'; +import { AnyEntity, EntityTarget } from '../../../types'; + +export function MapEntityNameToEntity( + entities: EntityClassOrSchema[] +): ValueProvider { + return { + provide: MAP_ENTITY, + useValue: entities.reduce( + (acum, item) => acum.set(camelToKebab(getEntityName(item)), item), + new Map>() + ), + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts new file mode 100644 index 00000000..620b2b52 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts @@ -0,0 +1,22 @@ +import { FactoryProvider } from '@nestjs/common'; +import { MAP_CONTROLLER_ENTITY, ZOD_INPUT_OPERATION } from '../constants'; +import { MapController } from '../types'; +import { zodInputOperation, ZodInputOperation } from '../utils'; +import { FIELD_FOR_ENTITY } from '../../../constants'; +import { GetFieldForEntity } from '../../mixin/types'; +import { ObjectLiteral } from '../../../types'; + +export function ZodInputOperation(): FactoryProvider< + ZodInputOperation +> { + return { + provide: ZOD_INPUT_OPERATION, + useFactory( + mapController: MapController, + getField: GetFieldForEntity + ) { + return zodInputOperation(mapController, getField); + }, + inject: [MAP_CONTROLLER_ENTITY, FIELD_FOR_ENTITY], + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts new file mode 100644 index 00000000..81ce0c2a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts @@ -0,0 +1 @@ +export * from './atomic-operation.module'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts new file mode 100644 index 00000000..643405e1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts @@ -0,0 +1,75 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ZodError } from 'zod'; +import { + InternalServerErrorException, + BadRequestException, +} from '@nestjs/common'; + +import { InputOperationPipe } from './input-operation.pipe'; + +import { KEY_MAIN_INPUT_SCHEMA, ZOD_INPUT_OPERATION } from '../constants'; +import { ZodInputOperation } from '../utils'; + +describe('PatchInputPipe', () => { + let patchInputPipe: InputOperationPipe; + let zodInputOperation: ZodInputOperation; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ZOD_INPUT_OPERATION, + useValue: { + parse() {}, + }, + }, + InputOperationPipe, + ], + }).compile(); + + patchInputPipe = module.get(InputOperationPipe); + zodInputOperation = module.get(ZOD_INPUT_OPERATION); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('It should be ok', () => { + const data = { + some: 'data', + }; + const check = { + [KEY_MAIN_INPUT_SCHEMA]: data, + }; + jest + .spyOn(zodInputOperation, 'parse') + .mockImplementationOnce(() => check as any); + expect(patchInputPipe.transform(check)).toEqual(data); + }); + + it('Should be not ok', () => { + jest.spyOn(zodInputOperation, 'parse').mockImplementationOnce(() => { + throw new ZodError([]); + }); + expect.assertions(1); + try { + patchInputPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + it('Should be 500', () => { + jest.spyOn(zodInputOperation, 'parse').mockImplementationOnce(() => { + throw new Error('Error mock'); + }); + expect.assertions(1); + + try { + patchInputPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(InternalServerErrorException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts new file mode 100644 index 00000000..a920101f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts @@ -0,0 +1,32 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { errorMap } from 'zod-validation-error'; +import { ZodError } from 'zod'; +import { JSONValue } from '../../mixin/types'; +import { InputArray, ZodInputOperation } from '../utils'; +import { KEY_MAIN_INPUT_SCHEMA, ZOD_INPUT_OPERATION } from '../constants'; + +export class InputOperationPipe + implements PipeTransform +{ + @Inject(ZOD_INPUT_OPERATION) + private zodInputOperation!: ZodInputOperation; + + transform(value: JSONValue): InputArray { + try { + return this.zodInputOperation.parse(value, { + errorMap: errorMap, + })[KEY_MAIN_INPUT_SCHEMA]; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts new file mode 100644 index 00000000..45723122 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts @@ -0,0 +1,609 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ModuleRef } from '@nestjs/core'; +import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; +import { ExecuteService, isZodError } from './execute.service'; +import { IterateFactory } from '../factory'; +import { + ASYNC_ITERATOR_FACTORY, + KEY_MAIN_INPUT_SCHEMA, + MAP_CONTROLLER_INTERCEPTORS, + OPTIONS, +} from '../constants'; + +import { + HttpException, + NotFoundException, + ParseIntPipe, + ParseBoolPipe, +} from '@nestjs/common'; +import { ParamsForExecute } from '../types'; +import { AsyncLocalStorage } from 'async_hooks'; +import { RUN_IN_TRANSACTION_FUNCTION } from '../../../constants'; + +describe('ExecuteService', () => { + let service: ExecuteService; + let runInTransaction: jest.Mock; + let moduleRef: ModuleRef; + let asyncIteratorFactory: IterateFactory; + const mapControllerInterceptors = new Map(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ExecuteService, + { + provide: RUN_IN_TRANSACTION_FUNCTION, + useValue: jest.fn(), + }, + { + provide: ModuleRef, + useValue: { + get() {}, + }, + }, + { + provide: OPTIONS, + useValue: {}, + }, + { + provide: ASYNC_ITERATOR_FACTORY, + useValue: { + createIterator: () => {}, + }, + }, + { + provide: MAP_CONTROLLER_INTERCEPTORS, + useValue: mapControllerInterceptors, + }, + { + provide: AsyncLocalStorage, + useValue: new AsyncLocalStorage(), + }, + ], + }).compile(); + + service = module.get(ExecuteService); + runInTransaction = module.get(RUN_IN_TRANSACTION_FUNCTION); + moduleRef = module.get(ModuleRef); + asyncIteratorFactory = module.get(ASYNC_ITERATOR_FACTORY); + mapControllerInterceptors.clear(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('run', () => { + it('should throw NotFoundException if controller not found', async () => { + const params = [ + { + controller: { name: 'NonExistentController' }, + module: { controllers: new Map() }, + }, + ] as ParamsForExecute[]; + + runInTransaction.mockImplementationOnce((args: () => {}) => args()); + + jest.spyOn(service as any, 'executeOperations').mockImplementation(() => { + throw new NotFoundException(); + }); + + await expect(service.run(params, [])).rejects.toThrow(NotFoundException); + + expect(runInTransaction).toHaveBeenCalled(); + }); + + it('should return an empty array if no operations are executed', async () => { + const params: ParamsForExecute[] = []; + + runInTransaction.mockImplementationOnce((args: () => {}) => args()); + jest.spyOn(service as any, 'executeOperations').mockReturnValue([]); + + const result = await service.run(params, []); + expect(result).toEqual([]); + expect(runInTransaction).toHaveBeenCalled(); + }); + }); + + describe('executeOperations', () => { + it('should correctly execute operations', async () => { + const params: ParamsForExecute[] = [ + { + controller: { name: 'TestController' }, + methodName: 'someMethod', + }, + ] as unknown as ParamsForExecute[]; + const callback = jest.fn().mockReturnValue({ value: 'test' }); + const mapController = { + someMethod: callback, + }; + jest + .spyOn(service as any, 'getControllerInstance') + .mockReturnValue(mapController); + + mapControllerInterceptors.set(mapController, new Map([[callback, []]])); + let callCount = 0; + jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ + [Symbol.asyncIterator]: () => + ({ + next: () => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ value: 'test', done: false }); + } else { + return Promise.resolve({ value: undefined, done: true }); + } + }, + } as any), + }); + + const result = await (service as any).executeOperations(params, []); + + expect(result).toEqual([{ value: 'test' }]); + }); + + it('should return an empty array if controller method does not return an object', async () => { + const params: ParamsForExecute[] = [ + { + controller: { name: 'TestController' }, + methodName: 'someMethod', + }, + ] as unknown as ParamsForExecute[]; + + const callback = jest.fn().mockReturnValue('not an object'); + const mapController = { + someMethod: callback, + }; + jest + .spyOn(service as any, 'getControllerInstance') + .mockReturnValue(mapController); + + mapControllerInterceptors.set(mapController, new Map([[callback, []]])); + + let callCount = 0; + jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ + [Symbol.asyncIterator]: () => + ({ + next: () => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ value: 'not an object', done: false }); + } else { + return Promise.resolve({ value: undefined, done: true }); + } + }, + } as any), + }); + + const result = await (service as any).executeOperations(params, []); + + expect(result).toEqual([]); + }); + + it('should call processException if an exception is thrown during execution', async () => { + const params: ParamsForExecute[] = [ + { + controller: { name: 'TestController' }, + methodName: 'someMethod', + }, + ] as unknown as ParamsForExecute[]; + + const callback = jest.fn().mockImplementation(() => { + throw new HttpException('Test exception', 400); + }); + const mapController = { + someMethod: callback, + }; + jest + .spyOn(service as any, 'getControllerInstance') + .mockReturnValue(mapController); + + mapControllerInterceptors.set(mapController, new Map([[callback, []]])); + + let callCount = 0; + jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ + [Symbol.asyncIterator]: () => + ({ + next: () => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ value: 'test', done: false }); + } else { + return Promise.resolve({ value: undefined, done: true }); + } + }, + } as any), + }); + + const processExceptionSpy = jest.spyOn( + service as any, + 'processException' + ); + + await expect((service as any).executeOperations(params)).rejects.toThrow( + HttpException + ); + + expect(processExceptionSpy).toHaveBeenCalled(); + }); + }); + + describe('getControllerInstance', () => { + it('should throw NotFoundException if controller not found', () => { + const params: ParamsForExecute = { + controller: { name: 'NonExistentController' }, + module: { controllers: new Map() }, + } as unknown as ParamsForExecute; + + expect(() => (service as any).getControllerInstance(params)).toThrow( + NotFoundException + ); + }); + + it('should return controller instance if controller is found', () => { + const controllerInstance = { + someMethod: jest.fn().mockReturnValue('test'), + }; + function TestController() {} + const params: ParamsForExecute = { + controller: TestController, + methodName: 'someMethod', + module: { + controllers: new Map([ + [TestController, { instance: controllerInstance }], + ]), + }, + } as unknown as ParamsForExecute; + + const result = (service as any).getControllerInstance(params); + + expect(result).toBe(controllerInstance); + }); + }); + + describe('processException', () => { + it('should rethrow HttpException with modified response if ZodError is thrown', () => { + const exception = new HttpException( + { + message: [{ path: ['test'] }], + }, + 400 + ); + + try { + (service as any).processException(exception, 1); + } catch (e) { + if (e instanceof HttpException) { + const response = e.getResponse(); + if (isZodError(response)) { + expect(response['message'][0]['path']).toEqual([ + KEY_MAIN_INPUT_SCHEMA, + '1', + 'test', + ]); + } else { + fail('Exception response is not a ZodError'); + } + } else { + fail('Caught exception is not a HttpException'); + } + } + }); + + it('should rethrow the original exception if it is not a HttpException', () => { + const exception = new Error('Test exception'); + + expect(() => (service as any).processException(exception, 1)).toThrow( + Error + ); + }); + }); + + describe('runOneOperation', () => { + it('should correctly run operation', async () => { + const controllerInstance = { + someMethod: jest.fn().mockReturnValue('test'), + }; + function TestController() {} + const pipes = [ + { index: 0, pipes: [] }, + { index: 1, pipes: [] }, + ]; + const params: ParamsForExecute = { + controller: TestController, + methodName: 'someMethod', + module: { + controllers: new Map([ + [TestController, { instance: controllerInstance }], + ]), + }, + params: ['param1', 'param2'], + } as unknown as ParamsForExecute; + + Reflect.defineMetadata( + ROUTE_ARGS_METADATA, + { 0: pipes[0], 1: pipes[1] }, + TestController, + 'someMethod' + ); + + const runPipesSpy = jest + .spyOn(service as any, 'runPipes') + .mockImplementation((param) => `modified_${param}`); + + await (service as any).runOneOperation(params); + + expect(runPipesSpy).toHaveBeenCalledWith( + 'param1', + params.module, + pipes[0].pipes + ); + expect(runPipesSpy).toHaveBeenCalledWith( + 'param2', + params.module, + pipes[1].pipes + ); + }); + + it('should not call runPipes if metadata is empty', async () => { + const controllerInstance = { + someMethod: jest.fn().mockReturnValue('test'), + }; + function TestController() {} + const params: ParamsForExecute = { + controller: TestController, + methodName: 'someMethod', + module: { + controllers: new Map([ + [TestController, { instance: controllerInstance }], + ]), + }, + params: ['param1', 'param2'], + } as unknown as ParamsForExecute; + + Reflect.defineMetadata( + ROUTE_ARGS_METADATA, + {}, + TestController, + 'someMethod' + ); + + const runPipesSpy = jest + .spyOn(service as any, 'runPipes') + .mockImplementation((param) => `modified_${param}`); + + await (service as any).runOneOperation(params); + + expect(runPipesSpy).not.toHaveBeenCalled(); + }); + }); + + describe('runPipes', () => { + it('should correctly run pipes', async () => { + const value = 'test'; + const pipes = [new ParseBoolPipe(), new ParseIntPipe()]; + const module = {} as any; + + jest + .spyOn(pipes[0], 'transform') + // @ts-ignore + .mockImplementation((val) => `validated_${val}`); + + jest + .spyOn(pipes[1], 'transform') + // @ts-ignore + .mockImplementation((val) => `parsed_${val}`); + const getPipeInstanceSpy = jest + .spyOn(service as any, 'getPipeInstance') + .mockImplementation((pipe) => + pipe instanceof ParseBoolPipe ? pipes[0] : pipes[1] + ); + + const result = await (service as any).runPipes(value, module, [ + pipes[0], + pipes[1], + ]); + + expect(result).toBe('parsed_validated_test'); + expect(getPipeInstanceSpy).toHaveBeenCalledTimes(2); + expect(getPipeInstanceSpy).toHaveBeenNthCalledWith(1, pipes[0], module); + expect(getPipeInstanceSpy).toHaveBeenNthCalledWith(2, pipes[1], module); + }); + + it('should not call getPipeInstance if pipes array is empty', async () => { + const value = 'test'; + const module = {} as any; + + const getPipeInstanceSpy = jest.spyOn(service as any, 'getPipeInstance'); + + const result = await (service as any).runPipes(value, module, []); + + expect(result).toBe('test'); + expect(getPipeInstanceSpy).not.toHaveBeenCalled(); + }); + }); + + describe('getPipeInstance', () => { + it('should return pipe instance from module if it exists', () => { + const pipe = new ParseBoolPipe(); + const module = { + getProviderByKey: jest.fn().mockReturnValue({ instance: pipe }), + } as any; + + const result = (service as any).getPipeInstance(ParseBoolPipe, module); + + expect(result).toBe(pipe); + expect(module.getProviderByKey).toHaveBeenCalledWith(ParseBoolPipe); + }); + + it('should return pipe instance from moduleRef if it does not exist in module', () => { + const pipe = new ParseBoolPipe(); + const module = { + getProviderByKey: jest.fn().mockReturnValue(null), + } as any; + jest.spyOn(service['moduleRef'], 'get').mockReturnValue(pipe); + + const result = (service as any).getPipeInstance(ParseBoolPipe, module); + + expect(result).toBe(pipe); + expect(service['moduleRef'].get).toHaveBeenCalledWith(ParseBoolPipe, { + strict: false, + }); + }); + }); + + describe('ExecuteService - replaceTmpIds', () => { + let service: ExecuteService; + + beforeEach(() => { + service = new ExecuteService(); + }); + + it('should be return id first input params of array is undefined', () => { + const inputParams = [] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is string', () => { + const inputParams = ['string'] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is number', () => { + const inputParams = [1] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + it('should be return id first input params of array is array', () => { + const inputParams = [[]] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is object', () => { + const inputParams = [{}] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is object with undefined of relationships', () => { + const inputParams = [{ relationships: undefined }] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be not replace of relation with object', () => { + const inputParams = [ + { + relationships: { + addresses: { data: { id: '1234', type: 'addresses' } }, + }, + }, + ] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be not replace of relation with array of object', () => { + const inputParams = [ + { + relationships: { + addresses: { + data: [ + { id: '1234', type: 'addresses' }, + { id: '1235', type: 'addresses' }, + ], + }, + }, + }, + ] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be replace of relation with object', () => { + const inputParams = [ + { + relationships: { + addresses: { data: { id: '1234', type: 'addresses' } }, + }, + }, + ] as any; + const newId = '4321'; + const tmpIdsMap = { '1234': newId }; + + const checkResult = [ + { + ...inputParams[0], + relationships: { + ...inputParams[0].relationships, + addresses: { + ...inputParams[0].relationships.addresses, + data: { + ...inputParams[0].relationships.addresses.data, + id: newId, + }, + }, + }, + }, + ]; + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(checkResult); + }); + + it('should be replace of relation with array of object', () => { + const inputParams = [ + { + relationships: { + addresses: { + data: [ + { id: '12345', type: 'addresses' }, + { id: '1234', type: 'addresses' }, + ], + }, + }, + }, + ] as any; + const newId = '4321'; + const tmpIdsMap = { '1234': newId }; + + const checkResult = [ + { + ...inputParams[0], + relationships: { + ...inputParams[0].relationships, + addresses: { + ...inputParams[0].relationships.addresses, + data: [ + inputParams[0].relationships.addresses.data[0], + { + ...inputParams[0].relationships.addresses.data[1], + id: newId, + }, + ], + }, + }, + }, + ]; + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(checkResult); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts new file mode 100644 index 00000000..0afc0f95 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts @@ -0,0 +1,338 @@ +import { + HttpException, + NotFoundException, + Inject, + Injectable, + PipeTransform, + Type, +} from '@nestjs/common'; +import { + INTERCEPTORS_METADATA, + ROUTE_ARGS_METADATA, +} from '@nestjs/common/constants'; +import { Module } from '@nestjs/core/injector/module'; +import { ArgumentMetadata } from '@nestjs/common/interfaces/features/pipe-transform.interface'; +import { ApplicationConfig, ModuleRef, NestContainer } from '@nestjs/core'; +import { + ObjectTyped, + ResourceObject, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; +import { + InterceptorsConsumer, + InterceptorsContextCreator, +} from '@nestjs/core/interceptors'; +import { Controller } from '@nestjs/common/interfaces'; +import { lastValueFrom } from 'rxjs'; +import { AsyncLocalStorage } from 'async_hooks'; + +import { MapControllerInterceptor, ParamsForExecute } from '../types'; +import { + ASYNC_ITERATOR_FACTORY, + KEY_MAIN_INPUT_SCHEMA, + MAP_CONTROLLER_INTERCEPTORS, +} from '../constants'; +import { IterateFactory } from '../factory'; +import { TypeFromType } from '../../mixin/types'; +import { RunInTransaction, ValidateQueryError } from '../../../types'; +import { RUN_IN_TRANSACTION_FUNCTION } from '../../../constants'; + +export function isZodError( + param: string | unknown +): param is { message: ValidateQueryError[] } { + return ( + param instanceof Object && + 'message' in param && + Array.isArray(param.message) && + 'path' in param.message[0] + ); +} + +@Injectable() +export class ExecuteService { + // @Inject(CURRENT_DATA_SOURCE_TOKEN) private readonly dataSource!: DataSource; + @Inject(ModuleRef) private readonly moduleRef!: ModuleRef & { + container: NestContainer; + applicationConfig: ApplicationConfig; + _moduleKey: string; + }; + @Inject(ASYNC_ITERATOR_FACTORY) private asyncIteratorFactory!: IterateFactory< + ExecuteService['runOneOperation'] + >; + @Inject(RUN_IN_TRANSACTION_FUNCTION) + private runInTransaction!: RunInTransaction< + () => ReturnType + >; + @Inject(MAP_CONTROLLER_INTERCEPTORS) + private mapControllerInterceptor!: MapControllerInterceptor; + + @Inject(AsyncLocalStorage) private asyncLocalStorage!: AsyncLocalStorage; + + private _interceptorsContextCreator!: InterceptorsContextCreator; + + get interceptorsContextCreator() { + if (!this._interceptorsContextCreator) { + this._interceptorsContextCreator = new InterceptorsContextCreator( + this.moduleRef.container, + this.moduleRef.applicationConfig + ); + } + + return this._interceptorsContextCreator; + } + + private interceptorsConsumer = new InterceptorsConsumer(); + + async run(params: ParamsForExecute[], tmpIds: (string | number)[]) { + return this.runInTransaction(() => this.executeOperations(params, tmpIds)); + } + + protected async executeOperations( + params: ParamsForExecute[], + tmpIds: (string | number)[] = [] + ) { + const iterateParams = this.asyncIteratorFactory.createIterator( + params as Parameters, + this.runOneOperation.bind(this) as ExecuteService['runOneOperation'] + ); + + const resultArray: Array< + ResourceObject | ResourceObjectRelationships + > = []; + let i = 0; + const tmpIdsMap: Record = {}; + try { + for await (const item of iterateParams) { + const currentParams = params[i]; + const controller = this.getControllerInstance(currentParams); + const methodName = + currentParams.methodName as (typeof currentParams)['methodName']; + + const paramsForExecute = item as unknown as ParamsForExecute['params']; + + const itemReplace = this.replaceTmpIds(paramsForExecute, tmpIdsMap); + const body = itemReplace.at(-1); + const currentTmpId = tmpIds[i]; + + if (methodName === 'postOne' && currentTmpId && body) { + if (typeof body === 'object' && 'attributes' in body) { + body['id'] = `${currentTmpId}`; + itemReplace[itemReplace.length - 1]; + } + } + + const interceptors = this.getInterceptorsArray( + controller, + controller[methodName], + currentParams.module + ); + + const result$: any = await this.interceptorsConsumer.intercept( + interceptors, + [ + ...Object.values(this.asyncLocalStorage.getStore() || {}), + itemReplace, + ], + controller, + // @ts-ignore + controller[methodName], + // @ts-ignore + async () => controller[methodName](...itemReplace) + ); + + const result = + interceptors.length === 0 + ? await result$ + : await lastValueFrom(result$); + + if (tmpIds[i] && result && !Array.isArray(result.data) && result.data) { + tmpIdsMap[tmpIds[i]] = result.data.id; + } + + if (result instanceof Object) { + resultArray.push(result); + } + i++; + } + } catch (e) { + this.processException(e, i); + } + return resultArray; + } + + private getInterceptorsArray( + controller: Controller, + callback: (...arg: any) => any, + module: ParamsForExecute['module'] + ) { + let controllerFromMap = this.mapControllerInterceptor.get(controller); + + if (!controllerFromMap) { + controllerFromMap = new Map(); + this.mapControllerInterceptor.set(controller, controllerFromMap); + } + + const interceptorsFromMap = controllerFromMap.get(callback); + + if (interceptorsFromMap) { + return interceptorsFromMap; + } + + const interceptorsForController = this.interceptorsContextCreator.create( + controller, + callback, + module.token + ); + + const interceptorsForMethode = new Set( + Reflect.getMetadata(INTERCEPTORS_METADATA, callback) || [] + ); + + const resultInterceptors = interceptorsForController.filter((i) => + interceptorsForMethode.has(i.constructor) + ); + controllerFromMap.set(callback, resultInterceptors); + return resultInterceptors; + } + + replaceTmpIds( + inputParams: T, + tmpIdsMap: Record + ): T { + const bodyInput = inputParams.at(-1); + if (!bodyInput) { + return inputParams; + } + if (typeof bodyInput === 'string') { + return inputParams; + } + if (typeof bodyInput === 'number') { + return inputParams; + } + + if (Array.isArray(bodyInput)) { + return inputParams; + } + + if (!(typeof bodyInput === 'object' && 'relationships' in bodyInput)) { + return inputParams; + } + + const { relationships } = bodyInput; + + if (!relationships) { + return inputParams; + } + + bodyInput.relationships = ObjectTyped.entries(relationships).reduce( + (acum, [name, val]) => { + if (!val) throw new Error('Va; undefined'); + const { data } = val; + if (Array.isArray(data)) { + acum[name] = { + data: data.map((i) => { + if (i === null) return i; + return { + ...i, + id: tmpIdsMap[i['id']] ? `${tmpIdsMap[i['id']]}` : i['id'], + }; + }), + }; + } else { + if (!data) { + acum[name] = val; + } else { + data['id'] = tmpIdsMap[data['id']] + ? `${tmpIdsMap[data['id']]}` + : data['id']; + acum[name] = { + data, + }; + } + } + return acum; + }, + { ...relationships } + ); + + inputParams[inputParams.length - 1] = bodyInput; + return inputParams; + } + + private getControllerInstance(params: ParamsForExecute) { + const controllerClass = params.controller; + const controllerInstanceWrapper = + params.module.controllers.get(controllerClass); + + if (!controllerInstanceWrapper) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['type'], + message: `Controller "${controllerClass.name}" not found`, + }; + throw new NotFoundException([error]); + } + + return controllerInstanceWrapper.instance as TypeFromType< + ParamsForExecute['controller'] + >; + } + + private processException(e: any, i: number) { + if (e instanceof HttpException) { + const response = e.getResponse(); + if (isZodError(response)) { + response['message'] = response['message'].map((m: any) => { + m['path'] = [KEY_MAIN_INPUT_SCHEMA, `${i}`, ...m['path']]; + return m; + }); + } + throw new HttpException(response, e.getStatus()); + } + throw e; + } + + private async runOneOperation( + paramForExecute: ParamsForExecute + ): Promise { + const { params, controller, methodName, module } = paramForExecute; + const pramsPipe = Object.values( + Reflect.getMetadata(ROUTE_ARGS_METADATA, controller, methodName) + ) as unknown as { + index: number; + pipes: Type[]; + }[]; + const resultParams = new Array(params.length); + for (const { pipes, index } of pramsPipe) { + resultParams[index] = await this.runPipes(params[index], module, pipes); + } + return resultParams as unknown as ParamsForExecute['params']; + } + + private async runPipes( + initialParams: unknown, + module: Module, + pipes: Type[] + ) { + let modifiedParams = initialParams; + for (const pipe of pipes) { + const pipeInstance = this.getPipeInstance(pipe, module); + modifiedParams = await pipeInstance.transform( + modifiedParams, + {} as ArgumentMetadata + ); + } + return modifiedParams; + } + + private getPipeInstance( + pipe: Type, + module: Module + ): PipeTransform { + const instanceWrapper = module.getProviderByKey(pipe); + if (!instanceWrapper) { + return this.moduleRef.get(pipe, { strict: false }); + } + return instanceWrapper.instance; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts new file mode 100644 index 00000000..ec654d2d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts @@ -0,0 +1,97 @@ +import { Test } from '@nestjs/testing'; +import { ModulesContainer } from '@nestjs/core'; +import { + MAP_ENTITY, + MAP_CONTROLLER_ENTITY, + OPTIONS, + MAP_CONTROLLER_INTERCEPTORS, +} from '../constants'; +import { Operation } from '../utils'; +import { ExplorerService } from './explorer.service'; + +describe('ExplorerService', () => { + let service: ExplorerService; + class EntityName {} + class ControllerName {} + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + ExplorerService, + { + provide: ModulesContainer, + useValue: new Map([ + [ + 'TestModule', + { + controllers: new Map([['ControllerName', ControllerName]]), + }, + ], + ]), + }, + { + provide: MAP_ENTITY, + useValue: new Map([['EntityName', EntityName]]), + }, + { + provide: MAP_CONTROLLER_ENTITY, + useValue: new Map([[EntityName, ControllerName]]), + }, + { + provide: MAP_CONTROLLER_INTERCEPTORS, + useValue: new Map(), + }, + { + provide: OPTIONS, + useValue: {}, + }, + ], + }).compile(); + + service = moduleRef.get(ExplorerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getControllerByEntityName()', () => { + it('should return the correct controller for a given entity name', () => { + expect(service.getControllerByEntityName('EntityName')).toBeDefined(); + }); + }); + + describe('getMethodNameByParam()', () => { + it('should return the correct method name for given parameters', () => { + expect(service.getMethodNameByParam(Operation.add, 'id', 'rel')).toBe( + 'postRelationship' + ); + }); + }); + + describe('getParamsForMethod()', () => { + it('should return the correct parameters for a given method name', () => { + const data = { + ref: { + id: '1', + relationship: 'belongs-to', + type: 'TypeA', + }, + op: Operation.add, + data: {}, + }; + expect(service.getParamsForMethod('patchRelationship', data)).toEqual([ + data.ref.id, + data.ref.relationship, + { data: data.data }, + ]); + }); + }); + + describe('getModulesByController()', () => { + it('should return the correct module for a given controller', () => { + expect( + service.getModulesByController(ControllerName as any) + ).toBeDefined(); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts new file mode 100644 index 00000000..1ad13223 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts @@ -0,0 +1,109 @@ +import { Inject, Injectable, Type } from '@nestjs/common'; +import { Module } from '@nestjs/core/injector/module'; +import { ModulesContainer } from '@nestjs/core'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { MAP_CONTROLLER_ENTITY, MAP_ENTITY } from '../constants'; +import { MapController, MapEntity, OperationMethode } from '../types'; +import { ObjectLiteral as Entity } from '../../../types'; +import { InputArray, Operation } from '../utils'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, +} from '../../mixin/zod'; + +@Injectable() +export class ExplorerService { + @Inject(ModulesContainer) + private readonly modulesContainer!: ModulesContainer; + + @Inject(MAP_ENTITY) private readonly mapEntity!: MapEntity; + @Inject(MAP_CONTROLLER_ENTITY) private readonly mapController!: MapController; + + private mapModuleByController = new Map< + Type>, + Module + >(); + + getControllerByEntityName(entityName: string): Type> { + const entity = this.mapEntity.get(entityName); + if (!entity) { + throw new Error(); + } + + const controller = this.mapController.get(entity); + if (!controller) { + throw new Error(); + } + + return controller; + } + + getMethodNameByParam( + operation: Operation, + id?: string, + rel?: string + ): OperationMethode { + switch (operation) { + case Operation.add: + return id ? 'postRelationship' : 'postOne'; + case Operation.remove: + return rel ? 'deleteRelationship' : 'deleteOne'; + case Operation.update: + return rel ? 'patchRelationship' : 'patchOne'; + default: + throw new Error(); + } + } + + getParamsForMethod( + methodName: OperationMethode, + data: InputArray[number] + ): Parameters[typeof methodName]> { + const { op, ref, ...other } = data; + switch (methodName) { + case 'postOne': + return [other as PostData]; + case 'patchOne': + return [ref.id as string, other as PatchData]; + case 'deleteOne': + return [ref.id as string]; + case 'deleteRelationship': + return [ + ref.id as string, + ref.relationship as EntityRelation, + other as PostRelationshipData, + ]; + case 'patchRelationship': + return [ + ref.id as string, + ref.relationship as EntityRelation, + other as PatchRelationshipData, + ]; + case 'postRelationship': + return [ + ref.id as string, + ref.relationship as EntityRelation, + other as PostRelationshipData, + ]; + } + } + + getModulesByController(controllers: Type>): Module { + const module = this.mapModuleByController.get(controllers); + if (module) { + return module; + } + + const findModule = [...this.modulesContainer.values()].find((i) => + [...i.controllers.values()].find((c) => c.name === controllers.name) + ); + if (findModule) { + return findModule; + } + + throw new Error(); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts new file mode 100644 index 00000000..aaf75a95 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts @@ -0,0 +1,3 @@ +export * from './explorer.service'; +export * from './execute.service'; +export * from './swagger.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts new file mode 100644 index 00000000..cf263440 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts @@ -0,0 +1,97 @@ +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { generateSchema } from '@anatine/zod-openapi'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; + +import { OperationController } from '../controllers'; +import { ZodInputOperation } from '../utils'; +import { ZOD_INPUT_OPERATION } from '../constants'; + +@Injectable() +export class SwaggerService implements OnModuleInit { + @Inject(ModuleRef) private readonly moduleRef!: ModuleRef; + @Inject(ZOD_INPUT_OPERATION) + private typeZodInputOperation!: ZodInputOperation; + + private initSwagger() { + const operationControllerInst = this.moduleRef.get(OperationController); + if (!operationControllerInst) + throw new Error('OperationController not found'); + const controller = operationControllerInst.constructor.prototype; + const descriptor = Reflect.getOwnPropertyDescriptor(controller, 'index'); + if (!descriptor) + throw new Error(`Descriptor for controller OperationController is empty`); + + ApiTags('Atomic operation')(operationControllerInst.constructor); + ApiOperation({ + summary: `Atomic operation for several entity"`, + operationId: `atomic_operation`, + })(controller, 'index', descriptor); + + ApiBody({ + description: `Json api schema for new atomic operatiom`, + schema: generateSchema(this.typeZodInputOperation) as + | SchemaObject + | ReferenceObject, + required: true, + examples: { + allField: { + summary: 'Examples several operation', + description: 'Examples several operation', + value: { + ['atomic:operations']: [ + { + op: 'add', + ref: { + type: 'users', + }, + data: 'EntityPostOne', + }, + { + op: 'update', + ref: { + type: 'users', + id: '1', + }, + data: 'EntityPatchOne', + }, + { + op: 'remove', + ref: { + type: 'users', + id: '1', + }, + }, + { + op: 'add', + ref: { + type: 'users', + id: '1', + relationship: 'EntityRelationName', + }, + data: 'UsersPostRelationship', + }, + { + op: 'update', + ref: { + type: 'users', + id: '1', + relationship: 'EntityRelationName', + }, + data: 'UsersDeleteRelationship', + }, + ], + }, + }, + }, + })(controller, 'index', descriptor); + } + + onModuleInit(): void { + this.initSwagger(); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts new file mode 100644 index 00000000..57fe5ae5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts @@ -0,0 +1,33 @@ +import { NestInterceptor, Type } from '@nestjs/common'; +import { Module } from '@nestjs/core/injector/module'; +import { Controller } from '@nestjs/common/interfaces'; +import { EntityTarget, ObjectLiteral } from '../../../types'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; + +export type MapControllerInterceptor = Map< + Controller, + Map<(...arg: any) => any, NestInterceptor[]> +>; +export type MapController = Map< + EntityTarget, + Type +>; +export type MapEntity = Map< + string, + EntityTarget +>; + +export type OperationMethode = keyof Omit< + { [k in keyof JsonBaseController]: string }, + 'getAll' | 'getOne' | 'getRelationship' +>; + +export type ParamsForExecute< + E extends ObjectLiteral = ObjectLiteral, + O extends OperationMethode = OperationMethode +> = { + methodName: O; + controller: Type>; + params: Parameters[O]>; + module: Module; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts new file mode 100644 index 00000000..405fa85d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts @@ -0,0 +1 @@ +export * from './zod/zod-helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts new file mode 100644 index 00000000..9419ce57 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts @@ -0,0 +1,591 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { z, ZodError } from 'zod'; +import { + Operation, + ZodAdd, + zodAdd, + zodInputOperation, + ZodInputOperation, + zodOperationRel, + ZodOperationRel, + zodRemove, + ZodRemove, + zodUpdate, + ZodUpdate, +} from './zod-helper'; +import { Users } from '../../../../mock-utils'; +import { FIELD_FOR_ENTITY } from '../../../../constants'; +import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; +import { MapController } from '../../types'; +import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; +import { GetFieldForEntity, TupleOfEntityRelation } from '../../../mixin/types'; + +describe('ZodHelperSpec', () => { + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('zodAdd', () => { + it('should be correct', () => { + const user = 'user'; + const schema = zodAdd(user); + const check: z.infer> = { + op: Operation.add, + ref: { + type: user, + }, + data: {}, + }; + const check1: z.infer> = { + op: Operation.add, + ref: { + type: user, + }, + data: [{}], + }; + const check2: z.infer> = { + op: Operation.add, + ref: { + type: user, + }, + data: null, + }; + const check3: z.infer> = { + op: Operation.add, + ref: { + type: user, + }, + data: null, + }; + const checkArray = [check, check1, check2, check3]; + for (const item of checkArray) { + const result = schema.parse(item); + expect(result.op).toBe(Operation.add); + expect(result.ref.type).toBe(user); + expect(result).toHaveProperty('data'); + } + }); + it('should be not correct', () => { + const schema = zodAdd('user'); + const check = { + op: Operation.add, + ref: { + type: 'user', + }, + data: {}, + sdfsf: {}, + }; + const check1 = { + op: Operation.add, + ref: { + type: 'user', + }, + }; + const check2 = { + op: Operation.add, + ref: { + type: 'user', + sdsdf: 'ssdfdsf', + }, + data: {}, + }; + const check3 = { + op: Operation.add, + ref: { + type12: 'user', + }, + data: {}, + }; + const check4 = { + op: Operation.add, + ref: { + type: 'sdfsdf', + }, + data: {}, + }; + const check5 = { + op: 'sdfsdf', + ref: { + type: 'user', + }, + data: {}, + }; + + const checkArray = [check, check1, check2, check3, check4, check5]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + describe('zodUpdate', () => { + it('should be correct', () => { + const user = 'user'; + const schema = zodUpdate(user); + const check: z.infer> = { + op: Operation.update, + ref: { + type: 'user', + id: '1', + }, + data: {}, + }; + const checkArray = [check]; + for (const item of checkArray) { + const result = schema.parse(item); + expect(result.op).toBe(Operation.update); + expect(result.ref.type).toBe(user); + expect(result).toHaveProperty('data'); + } + }); + it('should be not correct', () => { + const schema = zodUpdate('user'); + const check = { + op: Operation.update, + ref: { + type: 'user', + id: '12', + }, + data: {}, + sdfsf: {}, + }; + const check1 = { + op: Operation.update, + ref: { + type: 'user', + id: '12', + }, + }; + const check2 = { + op: Operation.update, + ref: { + type: 'user', + id: '12', + sdsdf: 'ssdfdsf', + }, + data: {}, + }; + const check3 = { + op: Operation.update, + ref: { + type12: 'user', + id: '12', + }, + data: {}, + }; + const check4 = { + op: Operation.update, + ref: { + type: 'sdfsdf', + id: '12', + }, + data: {}, + }; + const check5 = { + op: 'sdfsdf', + ref: { + type: 'user', + id: '12', + }, + data: {}, + }; + const check6 = { + op: Operation.update, + ref: { + type: 'user', + }, + data: {}, + }; + + const checkArray = [ + check, + check1, + check2, + check3, + check4, + check5, + check6, + ]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + describe('zodRemove', () => { + it('should be correct', () => { + const user = 'user'; + const schema = zodRemove(user); + const check: z.infer> = { + op: Operation.remove, + ref: { + type: 'user', + id: '1', + }, + }; + const checkArray = [check]; + for (const item of checkArray) { + const result = schema.parse(item); + expect(result.op).toBe(Operation.remove); + expect(result.ref.type).toBe(user); + expect(result).not.toHaveProperty('data'); + } + }); + + it('should be not correct', () => { + const schema = zodRemove('user'); + const check = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + }, + sdfsf: {}, + }; + const check1 = { + op: Operation.remove, + ref: { + type: 'user', + idsdf: '12', + }, + }; + const check2 = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + sdsdf: 'ssdfdsf', + }, + }; + const check3 = { + op: Operation.remove, + ref: { + type12: 'user', + id: '12', + }, + }; + const check4 = { + op: Operation.remove, + ref: { + type: 'sdfsdf', + id: '12', + }, + }; + const check5 = { + op: 'sdfsdf', + ref: { + type: 'user', + id: '12', + }, + }; + const check6 = { + op: Operation.remove, + ref: { + type: 'user', + }, + }; + + const checkArray = [ + check, + check1, + check2, + check3, + check4, + check5, + check6, + ]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + describe('zodOperationRel', () => { + it('should be correct', () => { + const user = 'user'; + const rel = [ + 'address', + 'notes', + ] as unknown as TupleOfEntityRelation; + const schema = zodOperationRel( + user, + rel, + Operation.remove + ); + const check: z.infer> = { + op: Operation.remove, + ref: { + type: 'user', + id: '1', + relationship: 'notes', + }, + data: { + id: 1, + type: 'notes', + }, + }; + const checkArray = [check]; + for (const item of checkArray) { + const result = schema.parse(item); + expect(result.op).toBe(Operation.remove); + expect(result.ref.type).toBe(user); + expect(result).toHaveProperty('data'); + expect(result['data']).toEqual(check.data); + } + }); + it('should be not correct', () => { + const user = 'user'; + const rel = [ + 'address', + 'notes', + ] as unknown as TupleOfEntityRelation; + const schema = zodOperationRel( + user, + rel, + Operation.remove + ); + + const check = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + relationship: 'notes', + }, + data: {}, + sdfsf: {}, + }; + const check1 = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + relationship: 'notes', + sdfsdf: 'sdfsdf', + }, + data: {}, + }; + const check2 = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + relationship1: 'notes', + }, + data: {}, + }; + const check3 = { + op: Operation.remove, + ref: { + type12: 'user', + id: '12', + relationship: 'notes', + }, + data: {}, + }; + const check4 = { + op: Operation.remove, + ref: { + type: 'sdfsdf', + id: '12', + relationship: 'notes', + }, + data: {}, + }; + const check5 = { + op: 'sdfsdf', + ref: { + type: 'user', + id: '12', + relationship: 'notes', + }, + data: {}, + }; + const check6 = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + relationship: 'notes1', + }, + data: {}, + }; + + const checkArray = [ + check, + check1, + check2, + check3, + check4, + check5, + check6, + ]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + describe('zodInputOperation', () => { + let getField: GetFieldForEntity; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: FIELD_FOR_ENTITY, + useValue: () => ({ + relations: [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ], + }), + }, + ], + }).compile(); + getField = module.get>(FIELD_FOR_ENTITY); + }); + + it('should be correct', () => { + const mapController: MapController = new Map([ + [Users as any, JsonBaseController], + ]); + const schema = zodInputOperation(mapController, getField); + const check: z.infer> = { + [KEY_MAIN_INPUT_SCHEMA]: [ + { + data: {}, + op: Operation.update, + ref: { + type: 'users', + relationship: 'manager', + id: '1', + }, + }, + { + data: {}, + op: Operation.update, + ref: { + type: 'users', + id: '1', + }, + }, + { + data: {}, + op: Operation.add, + ref: { + type: 'users', + }, + }, + { + op: Operation.remove, + ref: { + type: 'users', + id: '1', + }, + }, + ], + }; + expect(schema.parse(check)).toEqual(check); + }); + + it('incorrect input main data', () => { + const mapController: MapController = new Map([ + [Users as any, JsonBaseController], + ]); + const schema = zodInputOperation(mapController, getField); + const check = {}; + const check1 = { + ssdf: 'sdfsdf', + }; + const check2 = { + [KEY_MAIN_INPUT_SCHEMA]: null, + }; + const check3 = { + [KEY_MAIN_INPUT_SCHEMA]: '', + }; + const check4 = { + [KEY_MAIN_INPUT_SCHEMA]: {}, + }; + const check5 = { + [KEY_MAIN_INPUT_SCHEMA]: [], + }; + const checkArray = [check, check1, check2, check3, check4, check5]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + + it('should be incorrect methode not allow', () => { + class Test extends JsonBaseController { + override deleteOne(id: string | number): Promise { + return super.deleteOne(id); + } + } + const mapController: MapController = new Map([ + [Users as any, Test], + ]); + const schema = zodInputOperation(mapController, getField); + const check: z.infer> = { + [KEY_MAIN_INPUT_SCHEMA]: [ + { + data: {}, + op: Operation.update, + ref: { + type: 'users', + relationship: 'manager', + id: '1', + }, + }, + ], + }; + const check1: z.infer> = { + [KEY_MAIN_INPUT_SCHEMA]: [ + { + data: {}, + op: Operation.remove, + ref: { + type: 'users1', + relationship: 'manager', + id: '1', + }, + }, + ], + }; + const checkArray = [check, check1]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts new file mode 100644 index 00000000..3b2734c7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts @@ -0,0 +1,176 @@ +import { + z, + ZodArray, + ZodLiteral, + ZodNumber, + ZodObject, + ZodOptional, + ZodString, + ZodType, + ZodUnion, +} from 'zod'; +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; + +import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; +import { MapController } from '../../types'; +import { GetFieldForEntity, TupleOfEntityRelation } from '../../../mixin/types'; +import { getEntityName } from '../../../mixin/helper'; +import { ObjectLiteral } from '../../../../types'; + +export enum Operation { + add = 'add', + update = 'update', + remove = 'remove', +} + +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; + +const jsonSchema: ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +const zodGeneralData = jsonSchema.nullable(); +type ZodGeneral = typeof zodGeneralData; + +export type ZodAdd = ReturnType>; +export const zodAdd = (type: T) => + z + .object({ + op: z.literal(Operation.add), + ref: z + .object({ + type: z.literal(type), + tmpId: z.union([z.number(), z.string()]).optional(), + }) + .strict(), + data: zodGeneralData, + }) + .strict(); + +export type ZodUpdate = ReturnType>; +export const zodUpdate = (type: T) => + z + .object({ + op: z.literal(Operation.update), + ref: z + .object({ + type: z.literal(type), + id: z.string(), + }) + .strict(), + data: zodGeneralData, + }) + .strict(); +export type ZodRemove = ReturnType>; +export const zodRemove = (type: T) => + z + .object({ + op: z.literal(Operation.remove), + ref: z + .object({ + type: z.literal(type), + id: z.string(), + }) + .strict(), + }) + .strict(); + +export type ZodOperationRel< + E extends ObjectLiteral, + O extends Operation +> = ReturnType>; + +export const zodOperationRel = ( + type: string, + rel: TupleOfEntityRelation, + typeOperation: O +) => { + const literalArray = rel.map((i) => z.literal(i)) as [ + ZodLiteral, + ZodLiteral, + ...ZodLiteral[] + ]; + + return z + .object({ + op: z.literal(typeOperation), + ref: z + .object({ + type: z.literal(type), + id: z.string(), + relationship: z.union(literalArray), + }) + .strict(), + data: zodGeneralData, + }) + .strict(); +}; +export type ZodInputArray = ZodArray< + ZodObject<{ + op: ZodLiteral; + ref: ZodObject<{ + type: ZodString; + id: ZodOptional; + relationship: ZodOptional; + tmpId: ZodOptional>; + }>; + data: ZodOptional; + }>, + 'atleastone' +>; + +export type ZodInputOperation = + ReturnType>; +export type InputOperation = z.infer< + ZodInputOperation +>; + +export type InputArray = z.infer; + +export function zodInputOperation( + mapController: MapController, + getField: GetFieldForEntity +) { + const array = [] as unknown as [ + ZodAdd, + ZodUpdate, + ZodRemove, + ZodOperationRel, + ZodOperationRel, + ZodOperationRel + ]; + for (const [entity, controller] of mapController.entries()) { + const typeName = camelToKebab(getEntityName(entity)); + const { relations } = getField(entity); + + const hasOwnProperty = (props: string) => + Object.prototype.hasOwnProperty.call(controller.prototype, props); + + if (hasOwnProperty('postOne')) { + array.push(zodAdd(typeName)); + } + if (hasOwnProperty('patchOne')) { + array.push(zodUpdate(typeName)); + } + if (hasOwnProperty('deleteOne')) { + array.push(zodRemove(typeName)); + } + if (hasOwnProperty('postRelationship')) { + array.push(zodOperationRel(typeName, relations, Operation.add)); + } + if (hasOwnProperty('deleteRelationship')) { + array.push(zodOperationRel(typeName, relations, Operation.remove)); + } + if (hasOwnProperty('patchRelationship')) { + array.push(zodOperationRel(typeName, relations, Operation.update)); + } + } + + return z + .object({ + [KEY_MAIN_INPUT_SCHEMA]: z.array(z.union(array)).nonempty(), + }) + .strict(); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/index.ts new file mode 100644 index 00000000..06b783b6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/index.ts @@ -0,0 +1,3 @@ +export * from './type-orm'; +export * from './micro-orm'; +export * from './atomic-operation'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts new file mode 100644 index 00000000..4b7a1069 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts @@ -0,0 +1,2 @@ +export * from './micro-orm.module'; +export * from './type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts new file mode 100644 index 00000000..c2b3095f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts @@ -0,0 +1,14 @@ +import { NestProvider, ResultModuleOptions, ObjectLiteral } from '../../types'; +import { DynamicModule } from '@nestjs/common'; + +export class MicroOrmModule { + static forRoot(options: ResultModuleOptions): DynamicModule { + return { + module: MicroOrmModule, + }; + } + + static getUtilProviders(entity: ObjectLiteral): NestProvider { + return []; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts new file mode 100644 index 00000000..6a557459 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts @@ -0,0 +1,3 @@ +export type MicroOrmParam = { + // testParam: boolean; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts new file mode 100644 index 00000000..4c256991 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts @@ -0,0 +1,9 @@ +import { Bindings, excludeMethod } from './bindings'; + +describe('bindings', () => { + it('excludeMethod', () => { + expect(excludeMethod(['patchRelationship'])).toEqual( + Object.keys(Bindings).filter((i) => i !== 'patchRelationship') + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts new file mode 100644 index 00000000..45eb5708 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts @@ -0,0 +1,202 @@ +import { Body, Param, Query, RequestMethod } from '@nestjs/common'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +import { BindingsConfig, MethodName } from '../types'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { PARAMS_RELATION_NAME, PARAMS_RESOURCE_ID } from '../../../constants'; + +import { + queryInputMixin, + queryMixin, + queryFiledInIncludeMixin, + queryCheckSelectFieldMixin, + idPipeMixin, + checkItemEntityPipeMixin, + postInputPipeMixin, + patchInputPipeMixin, + parseRelationshipNamePipeMixin, + postRelationshipPipeMixin, + patchRelationshipPipeMixin, +} from '../pipe'; + +const Bindings: BindingsConfig = { + getAll: { + method: RequestMethod.GET, + name: 'getAll', + path: '/', + implementation: JsonBaseController.prototype.getAll, + parameters: [ + { + decorator: Query, + mixins: [ + queryInputMixin, + queryMixin, + queryFiledInIncludeMixin, + queryCheckSelectFieldMixin, + ], + }, + ], + }, + getOne: { + method: RequestMethod.GET, + name: 'getOne', + path: `:${PARAMS_RESOURCE_ID}`, + implementation: JsonBaseController.prototype.getOne, + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + decorator: Query, + mixins: [ + queryInputMixin, + queryMixin, + queryFiledInIncludeMixin, + queryCheckSelectFieldMixin, + ], + }, + ], + }, + deleteOne: { + method: RequestMethod.DELETE, + name: 'deleteOne', + path: `:${PARAMS_RESOURCE_ID}`, + implementation: JsonBaseController.prototype.deleteOne, + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + ], + }, + postOne: { + method: RequestMethod.POST, + name: 'postOne', + path: '/', + implementation: JsonBaseController.prototype.postOne, + parameters: [ + { + decorator: Body, + mixins: [postInputPipeMixin], + }, + ], + }, + patchOne: { + method: RequestMethod.PATCH, + name: 'patchOne', + path: `:${PARAMS_RESOURCE_ID}`, + implementation: JsonBaseController.prototype.patchOne, + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + decorator: Body, + mixins: [patchInputPipeMixin], + }, + ], + }, + getRelationship: { + path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, + name: 'getRelationship', + method: RequestMethod.GET, + implementation: JsonBaseController.prototype.getRelationship, + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + property: PARAMS_RELATION_NAME, + decorator: Param, + mixins: [parseRelationshipNamePipeMixin], + }, + ], + }, + postRelationship: { + path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, + name: 'postRelationship', + method: RequestMethod.POST, + implementation: JsonBaseController.prototype['postRelationship'], + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + property: PARAMS_RELATION_NAME, + decorator: Param, + mixins: [parseRelationshipNamePipeMixin], + }, + { + decorator: Body, + mixins: [postRelationshipPipeMixin], + }, + ], + }, + deleteRelationship: { + path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, + name: 'deleteRelationship', + method: RequestMethod.DELETE, + implementation: JsonBaseController.prototype['deleteRelationship'], + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + property: PARAMS_RELATION_NAME, + decorator: Param, + mixins: [parseRelationshipNamePipeMixin], + }, + { + decorator: Body, + mixins: [postRelationshipPipeMixin], + }, + ], + }, + patchRelationship: { + path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, + name: 'patchRelationship', + method: RequestMethod.PATCH, + implementation: JsonBaseController.prototype['patchRelationship'], + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + property: PARAMS_RELATION_NAME, + decorator: Param, + mixins: [parseRelationshipNamePipeMixin], + }, + { + decorator: Body, + mixins: [patchRelationshipPipeMixin], + }, + ], + }, +}; + +export { Bindings }; + +export function excludeMethod( + names: Array> +): Array { + const tmpObject = names.reduce( + (acum, key) => ((acum[key] = true), acum), + {} as Record, boolean> + ); + return ObjectTyped.keys(Bindings).filter( + (method) => !tmpObject[method] + ) as Array; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts new file mode 100644 index 00000000..28aa9ce7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts @@ -0,0 +1,79 @@ +import { + EntityRelation, + ResourceObject, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; + +import { ORM_SERVICE_PROPS } from '../../../constants'; +import { MethodName } from '../types'; +import { ObjectLiteral } from '../../../types'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../zod'; +import { OrmService } from '../types'; + +type RequestMethodeObject = { + [K in MethodName]: OrmService[K]; +}; + +export class JsonBaseController + implements RequestMethodeObject +{ + private [ORM_SERVICE_PROPS]!: OrmService; + + getOne(id: string | number, query: QueryOne): Promise> { + return this[ORM_SERVICE_PROPS].getOne(id, query); + } + getAll(query: Query): Promise> { + return this[ORM_SERVICE_PROPS].getAll(query); + } + deleteOne(id: string | number): Promise { + return this[ORM_SERVICE_PROPS].deleteOne(id); + } + + patchOne( + id: string | number, + inputData: PatchData + ): Promise> { + return this[ORM_SERVICE_PROPS].patchOne(id, inputData); + } + + postOne(inputData: PostData): Promise> { + return this[ORM_SERVICE_PROPS].postOne(inputData); + } + + getRelationship>( + id: string | number, + relName: Rel + ): Promise> { + return this[ORM_SERVICE_PROPS].getRelationship(id, relName); + } + postRelationship>( + id: string | number, + relName: Rel, + input: PostRelationshipData + ): Promise> { + return this[ORM_SERVICE_PROPS].postRelationship(id, relName, input); + } + + deleteRelationship>( + id: string | number, + relName: Rel, + input: PostRelationshipData + ): Promise { + return this[ORM_SERVICE_PROPS].deleteRelationship(id, relName, input); + } + + patchRelationship>( + id: string | number, + relName: Rel, + input: PatchRelationshipData + ): Promise> { + return this[ORM_SERVICE_PROPS].patchRelationship(id, relName, input); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts new file mode 100644 index 00000000..2036a50b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts @@ -0,0 +1,2 @@ +export * from './json-api/json-api.decorator'; +export * from './inject-service/inject-service.decorator'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts new file mode 100644 index 00000000..e1485972 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts @@ -0,0 +1,30 @@ +import { + PROPERTY_DEPS_METADATA, + SELF_DECLARED_DEPS_METADATA, +} from '@nestjs/common/constants'; +import 'reflect-metadata'; + +import { InjectService } from './inject-service.decorator'; +import { ORM_SERVICE } from '../../../../constants'; + +describe('InjectServiceDecorator', () => { + it('should save property key', () => { + class SomeClass { + @InjectService() protected property: any; + constructor(@InjectService() protected test: any) {} + } + + const properties = Reflect.getMetadata(PROPERTY_DEPS_METADATA, SomeClass); + const properties1 = Reflect.getMetadata( + SELF_DECLARED_DEPS_METADATA, + SomeClass + ); + expect( + properties.find((item: any) => item.type === ORM_SERVICE) + ).toBeDefined(); + + expect( + properties1.find((item: any) => item.param === ORM_SERVICE) + ).toBeDefined(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts new file mode 100644 index 00000000..ab1c5157 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts @@ -0,0 +1,7 @@ +import { Inject } from '@nestjs/common'; + +import { ORM_SERVICE } from '../../../../constants'; + +export function InjectService(): PropertyDecorator & ParameterDecorator { + return Inject(ORM_SERVICE); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts new file mode 100644 index 00000000..fd246eb9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts @@ -0,0 +1,69 @@ +import 'reflect-metadata'; + +import { + JSON_API_DECORATOR_ENTITY, + JSON_API_DECORATOR_OPTIONS, +} from '../../../../constants'; +import { JsonApi } from './json-api.decorator'; + +import { excludeMethod, Bindings } from '../../config/bindings'; +import { DecoratorOptions } from '../../types'; + +describe('InjectServiceDecorator', () => { + it('should save entity in class', () => { + const testedEntity = class SomeEntity {}; + + @JsonApi(testedEntity) + class SomeClass {} + + const data = Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, SomeClass); + expect(data).toBe(testedEntity); + }); + + it('should save options in class', () => { + const testedEntity = class SomeEntity {}; + const apiOptions: DecoratorOptions = { + allowMethod: ['getAll', 'deleteRelationship'], + }; + + @JsonApi(testedEntity, apiOptions) + class SomeClass {} + + const data = Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, SomeClass); + expect(data).toEqual(apiOptions); + }); + + it('should save options in class using helpFunction', () => { + const testedEntity = class SomeEntity {}; + const example = ['getAll', 'deleteRelationship']; + const apiOptions: DecoratorOptions = { + allowMethod: excludeMethod(example as any), + }; + + @JsonApi(testedEntity, apiOptions) + class SomeClass {} + + const data: DecoratorOptions = Reflect.getMetadata( + JSON_API_DECORATOR_OPTIONS, + SomeClass + ); + expect(data).toEqual(apiOptions); + expect(data.allowMethod).toEqual( + Object.keys(Bindings).filter((k) => !example.includes(k)) + ); + }); + + it('should save options in class and correctly set overrideRoute', () => { + const testedEntity = class SomeEntity {}; + const apiOptions: DecoratorOptions = { + allowMethod: ['getAll', 'deleteRelationship'], + overrideRoute: '123', + }; + + @JsonApi(testedEntity, apiOptions) + class SomeClass {} + + const data = Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, SomeClass); + expect(data).toEqual(apiOptions); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts new file mode 100644 index 00000000..c73f8594 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts @@ -0,0 +1,19 @@ +import { + JSON_API_DECORATOR_ENTITY, + JSON_API_DECORATOR_OPTIONS, +} from '../../../../constants'; +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { DecoratorOptions } from '../../types'; + +export function JsonApi( + entity: EntityClass, + options?: DecoratorOptions +): ClassDecorator { + return (target): typeof target => { + Reflect.defineMetadata(JSON_API_DECORATOR_ENTITY, entity, target); + if (options) { + Reflect.defineMetadata(JSON_API_DECORATOR_OPTIONS, options, target); + } + return target; + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts new file mode 100644 index 00000000..0f09495c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts @@ -0,0 +1 @@ +export * from './zod-validate.factory'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts new file mode 100644 index 00000000..ca42707e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts @@ -0,0 +1,160 @@ +import { FactoryProvider, ValueProvider } from '@nestjs/common'; + +import { + PARAMS_FOR_ZOD_SCHEMA, + ZOD_INPUT_QUERY_SCHEMA, + ZOD_QUERY_SCHEMA, + ZOD_POST_SCHEMA, + ZOD_PATCH_SCHEMA, + ZOD_POST_RELATIONSHIP_SCHEMA, + ZOD_PATCH_RELATIONSHIP_SCHEMA, +} from '../../../constants'; + +import { + zodInputQuery, + ZodInputQuery, + zodQuery, + ZodQuery, + ZodPost, + zodPost, + zodPatch, + ZodPatch, + zodPostRelationship, + ZodPostRelationship, + zodPatchRelationship, + ZodPatchRelationship, +} from '../zod'; +import { ObjectLiteral } from '../../../types'; +import { EntityProps, ZodParams } from '../types'; + +export function ZodInputQuerySchema(): FactoryProvider< + ZodInputQuery +> { + return { + provide: ZOD_INPUT_QUERY_SCHEMA, + inject: [ + { + token: PARAMS_FOR_ZOD_SCHEMA, + optional: false, + }, + ], + useFactory: (zodParams: ZodParams>) => { + const { entityFieldsStructure, entityRelationStructure } = zodParams; + return zodInputQuery(entityFieldsStructure, entityRelationStructure); + }, + }; +} + +export function ZodQuerySchema(): FactoryProvider< + ZodQuery +> { + return { + provide: ZOD_QUERY_SCHEMA, + inject: [ + { + token: PARAMS_FOR_ZOD_SCHEMA, + optional: false, + }, + ], + useFactory: (zodParams: ZodParams>) => { + const { + entityFieldsStructure, + entityRelationStructure, + propsType, + propsArray, + } = zodParams; + return zodQuery( + entityFieldsStructure, + entityRelationStructure, + propsArray, + propsType + ); + }, + }; +} + +export function ZodPostSchema< + E extends ObjectLiteral, + I extends string +>(): FactoryProvider> { + return { + provide: ZOD_POST_SCHEMA, + inject: [ + { + token: PARAMS_FOR_ZOD_SCHEMA, + optional: false, + }, + ], + useFactory: (zodParams: ZodParams, I>) => { + const { + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType, + } = zodParams; + return zodPost( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + }, + }; +} + +export function ZodPatchSchema< + E extends ObjectLiteral, + I extends string +>(): FactoryProvider> { + return { + provide: ZOD_PATCH_SCHEMA, + inject: [ + { + token: PARAMS_FOR_ZOD_SCHEMA, + optional: false, + }, + ], + useFactory: (zodParams: ZodParams, I>) => { + const { + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType, + } = zodParams; + return zodPatch( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + }, + }; +} + +export const ZodInputPostRelationshipSchema: ValueProvider = + { + provide: ZOD_POST_RELATIONSHIP_SCHEMA, + useValue: zodPostRelationship, + }; + +export const ZodInputPatchRelationshipSchema: ValueProvider = + { + provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, + useValue: zodPatchRelationship, + }; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts new file mode 100644 index 00000000..72ff9d08 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts @@ -0,0 +1,165 @@ +import { + METHOD_METADATA, + PATH_METADATA, + ROUTE_ARGS_METADATA, +} from '@nestjs/common/constants'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; + +import { bindController } from './bind-controller'; +import { Users } from '../../../mock-utils'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; +import { + ParseIntPipe, + Query, + Body, + Param, + PipeTransform, + ArgumentMetadata, +} from '@nestjs/common'; +import { OrmService } from '../types'; +import { PatchData } from '../zod'; +import { JsonApi } from '../decorators'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { excludeMethod } from '../config/bindings'; + +import { Bindings } from '../config/bindings'; + +const mapParams = new Map(); +mapParams.set(Query, RouteParamtypes.QUERY); +mapParams.set(Body, RouteParamtypes.BODY); +mapParams.set(Param, RouteParamtypes.PARAM); + +describe('bindController', () => { + it('Should be all methode', () => { + class Controller {} + const config = { + requiredSelectField: false, + pipeForId: ParseIntPipe, + debug: false, + useSoftDelete: false, + } as any; + bindController(Controller, Users, config); + + expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ + 'constructor', + 'getAll', + 'getOne', + 'deleteOne', + 'postOne', + 'patchOne', + 'getRelationship', + 'postRelationship', + 'deleteRelationship', + 'patchRelationship', + ]); + + for (const [key, value] of ObjectTyped.entries(Bindings)) { + const descriptor = Reflect.getOwnPropertyDescriptor( + Controller.prototype, + key + ); + if (!descriptor) { + throw new Error('descriptor is empty:' + key); + } + + expect(Reflect.getMetadata(PATH_METADATA, descriptor.value)).toBe( + value.path + ); + expect(Reflect.getMetadata(METHOD_METADATA, descriptor.value)).toBe( + value.method + ); + const paramsMetadata = Reflect.getMetadata( + ROUTE_ARGS_METADATA, + Controller.prototype.constructor, + key + ); + for (const params in value.parameters) { + const tmp = value.parameters[params]; + if (!tmp.decorator) { + expect(paramsMetadata).toEqual(tmp.decorator); + continue; + } + const paramsMetadataItem = + paramsMetadata[`${mapParams.get(tmp.decorator)}:${params}`]; + expect(paramsMetadataItem).not.toEqual(undefined); + expect(paramsMetadataItem.index).toBe(parseInt(params)); + tmp.mixins.forEach((i, k) => { + expect(i(Users, config).name).toEqual( + paramsMetadataItem.pipes[k].name + ); + }); + } + } + }); + + it('Should be without methode: postOne, getRelationship', () => { + @JsonApi(Users, { + allowMethod: excludeMethod(['postOne', 'getRelationship']), + }) + class Controller {} + const config = { + requiredSelectField: false, + pipeForId: ParseIntPipe, + debug: false, + useSoftDelete: false, + } as any; + bindController(Controller, Users, config); + expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ + 'constructor', + 'getAll', + 'getOne', + 'deleteOne', + 'patchOne', + 'postRelationship', + 'deleteRelationship', + 'patchRelationship', + ]); + }); + + it('Should be use custom pipe', () => { + class SomePipes implements PipeTransform { + transform(value: any, metadata: ArgumentMetadata): any { + return undefined; + } + } + class Controller extends JsonBaseController { + override patchOne( + @Param('id', SomePipes) id: string | number, + @Body(SomePipes) inputData: PatchData + ): ReturnType['patchOne']> { + return super.patchOne(id, inputData); + } + } + const config = { + requiredSelectField: false, + pipeForId: SomePipes, + debug: false, + useSoftDelete: false, + } as any; + bindController(Controller, Users, config); + + const paramsMetadata = Reflect.getMetadata( + ROUTE_ARGS_METADATA, + Controller.prototype.constructor, + 'patchOne' + ); + expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes[0]).toEqual( + SomePipes + ); + + expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes.length).toBe( + Bindings.patchOne.parameters[0].mixins.length + 1 + ); + expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes.at(-1)).toEqual( + SomePipes + ); + + expect(paramsMetadata[`${mapParams.get(Body)}:1`].pipes.length).toBe( + Bindings.patchOne.parameters[1].mixins.length + 1 + ); + expect(paramsMetadata[`${mapParams.get(Body)}:1`].pipes.at(-1)).toEqual( + SomePipes + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts new file mode 100644 index 00000000..de5c6d66 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts @@ -0,0 +1,124 @@ +import { + Body, + Delete, + Get, + HttpCode, + Param, + Patch, + Post, + Query, + RequestMethod, +} from '@nestjs/common'; +import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; +import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; + +import { Bindings } from '../config/bindings'; +import { DecoratorOptions, MixinOptions, MethodName } from '../types'; +import { NestController, ExtractNestType } from '../../../types'; +import { JSON_API_DECORATOR_OPTIONS } from '../../../constants'; + +export function bindController( + controller: ExtractNestType, + entity: MixinOptions['entity'], + config: MixinOptions['config'] +): void { + for (const methodName in Bindings) { + const { name, path, parameters, method, implementation } = + Bindings[methodName as MethodName]; + + const decoratorOptions: DecoratorOptions = Reflect.getMetadata( + JSON_API_DECORATOR_OPTIONS, + controller + ); + if (decoratorOptions) { + const { allowMethod = Object.keys(Bindings) } = decoratorOptions; + if (!allowMethod.includes(name)) continue; + } + + if (!Object.prototype.hasOwnProperty.call(controller.prototype, name)) { + // need uniq descriptor for correct work swagger + Reflect.defineProperty(controller.prototype, name, { + value: function ( + ...arg: Parameters + ): ReturnType { + return this.constructor.__proto__.prototype[name].call(this, ...arg); + }, + writable: true, + enumerable: false, + configurable: true, + }); + } + + const descriptor = Reflect.getOwnPropertyDescriptor( + controller.prototype, + name + ); + + if (!descriptor) { + throw new Error( + `Descriptor for "${controller.name}[${name}]" is undefined` + ); + } + + switch (method) { + case RequestMethod.GET: { + Get(path)(controller.prototype, name, descriptor); + break; + } + case RequestMethod.DELETE: { + HttpCode(204)(controller.prototype, name, descriptor); + Delete(path)(controller.prototype, name, descriptor); + break; + } + case RequestMethod.POST: { + Post(path)(controller.prototype, name, descriptor); + break; + } + case RequestMethod.PATCH: { + Patch(path)(controller.prototype, name, descriptor); + break; + } + default: { + throw new Error(`Method '${method}' unsupported`); + } + } + const paramsMetadata = Reflect.getMetadata( + ROUTE_ARGS_METADATA, + controller.prototype.constructor, + name + ); + + for (const key in parameters) { + const parameter = parameters[key]; + const { property, decorator, mixins } = parameter; + const resultMixin = mixins.map((mixin) => mixin(entity, config)); + + if (paramsMetadata) { + let typeDecorator: RouteParamtypes; + switch (decorator) { + case Query: + typeDecorator = RouteParamtypes.QUERY; + break; + case Param: + typeDecorator = RouteParamtypes.PARAM; + break; + case Body: + typeDecorator = RouteParamtypes.BODY; + } + + const tmp = Object.entries(paramsMetadata) + .filter(([k, v]) => k.split(':').at(0) === typeDecorator.toString()) + .reduce( + (acum, [k, v]) => (acum.push(...(v as any).pipes), acum), + [] as any + ); + resultMixin.push(...tmp); + } + decorator(property, ...resultMixin)( + controller.prototype, + name, + parseInt(key, 10) + ); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts new file mode 100644 index 00000000..8ca13ac8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts @@ -0,0 +1,95 @@ +import { + CONTROLLER_WATERMARK, + INTERCEPTORS_METADATA, + PATH_METADATA, + PROPERTY_DEPS_METADATA, +} from '@nestjs/common/constants'; +import { createController } from './create-controller'; +import { Users } from '../../../mock-utils'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { + JSON_API_CONTROLLER_POSTFIX, + ORM_SERVICE, + ORM_SERVICE_PROPS, +} from '../../../constants'; +import { InjectService, JsonApi } from '../decorators'; +import { ErrorInterceptors, LogTimeInterceptors } from '../interceptors'; + +describe('createController', () => { + it('Should be error', () => { + class TestController {} + expect.assertions(2); + try { + createController(Users, TestController); + } catch (e) { + expect(e).toBeInstanceOf(Error); + expect((e as Error).message).toBe( + 'Controller "TestController" should be inherited of "JsonBaseController"' + ); + } + }); + + it('Should be correct name controller', () => { + class TestController extends JsonBaseController {} + const result = createController(Users); + const result1 = createController(Users, TestController); + expect(result.name).toBe('Users' + JSON_API_CONTROLLER_POSTFIX); + expect(result1.name).toBe('TestController'); + }); + + it('Should be correct path for controller', () => { + const overrideRoute = 'override-route'; + class TestController extends JsonBaseController {} + + @JsonApi(Users, { + overrideRoute, + }) + class TestController2 extends JsonBaseController {} + const result = createController(Users); + const result2 = createController(Users, TestController); + const result3 = createController(Users, TestController2); + + expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result)).toBe(true); + expect(Reflect.getMetadata(PATH_METADATA, result)).toBe('users'); + + expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result2)).toBe(true); + expect(Reflect.getMetadata(PATH_METADATA, result2)).toBe('users'); + + expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result3)).toBe(true); + expect(Reflect.getMetadata(PATH_METADATA, result3)).toBe(overrideRoute); + }); + + it('Check inject typeorm, service', () => { + class TestController extends JsonBaseController { + @InjectService() private tmp: any; + } + + const result = createController(Users); + const result1 = createController(Users, TestController); + + const check = Reflect.getMetadata( + PROPERTY_DEPS_METADATA, + result.prototype.constructor + ); + const check1 = Reflect.getMetadata( + PROPERTY_DEPS_METADATA, + result1.prototype.constructor + ); + + const intecept = Reflect.getMetadata( + INTERCEPTORS_METADATA, + result1.prototype.constructor + ); + expect(intecept).not.toBe(undefined); + expect(intecept[0]).toEqual(LogTimeInterceptors); + expect(intecept[1]).toEqual(ErrorInterceptors); + expect(check[0].key).toBe(ORM_SERVICE_PROPS); + expect(check[0].type).toEqual(ORM_SERVICE); + + expect(check1[0].key).toBe('tmp'); + expect(check1[0].type).toEqual(ORM_SERVICE); + + expect(check1[1].key).toBe(ORM_SERVICE_PROPS); + expect(check1[1].type).toEqual(ORM_SERVICE); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts new file mode 100644 index 00000000..bab373a3 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts @@ -0,0 +1,53 @@ +import { Controller, Inject, Type, UseInterceptors } from '@nestjs/common'; + +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; + +import { getProviderName, nameIt } from './utils'; +import { + JSON_API_CONTROLLER_POSTFIX, + JSON_API_DECORATOR_OPTIONS, + ORM_SERVICE, + ORM_SERVICE_PROPS, +} from '../../../constants'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { ErrorInterceptors, LogTimeInterceptors } from '../interceptors'; + +import { DecoratorOptions, MixinOptions } from '../types'; + +export function createController( + entity: MixinOptions['entity'], + controller?: MixinOptions['controller'] +): Type { + const controllerClass = + controller || + nameIt( + getProviderName(entity, JSON_API_CONTROLLER_POSTFIX), + JsonBaseController + ); + + const entityName = entity.name; + + if ( + !Object.prototype.isPrototypeOf.call(JsonBaseController, controllerClass) + ) { + throw new Error( + `Controller "${controller?.name}" should be inherited of "JsonBaseController"` + ); + } + + const decoratorOptions: DecoratorOptions = Reflect.getMetadata( + JSON_API_DECORATOR_OPTIONS, + controllerClass + ); + + const controllerPath = + decoratorOptions && decoratorOptions['overrideRoute'] + ? decoratorOptions['overrideRoute'].toString() + : `${camelToKebab(entityName)}`; + Controller(controllerPath)(controllerClass); + + Inject(ORM_SERVICE)(controllerClass.prototype, ORM_SERVICE_PROPS); + UseInterceptors(LogTimeInterceptors)(controllerClass); + UseInterceptors(ErrorInterceptors)(controllerClass); + return controllerClass; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts new file mode 100644 index 00000000..7f0e8bc2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts @@ -0,0 +1,3 @@ +export * from './utils'; +export * from './create-controller'; +export * from './bind-controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts new file mode 100644 index 00000000..fd1d708e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts @@ -0,0 +1,17 @@ +import { getEntityName, nameIt } from './'; + +describe('Test utils', () => { + it('getEntityName', () => { + expect(getEntityName('Entity')).toBe('Entity'); + expect(getEntityName(class EntityClass {})).toBe('EntityClass'); + class EntityClassInst {} + const tmp = new EntityClassInst(); + expect(getEntityName(tmp as any)).toBe('EntityClassInst'); + }); + + it('nameIt', () => { + const newNameClass = 'newNameClass'; + const newClass = nameIt(newNameClass, class {}); + expect(getEntityName(newClass)).toBe(newNameClass); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts new file mode 100644 index 00000000..8aa2b798 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts @@ -0,0 +1,41 @@ +import { EntityTarget, ObjectLiteral } from '../../../types'; + +import { upperFirstLetter } from '@klerick/json-api-nestjs-shared'; + +export const nameIt = ( + name: string, + cls: new (...rest: unknown[]) => Record +) => + ({ + [name]: class extends cls { + constructor(...arg: unknown[]) { + super(...arg); + } + }, + }[name]); + +export const getEntityName = ( + entity: EntityTarget +): string => { + if (typeof entity === 'string') { + return entity; + } + + if ('name' in entity) { + return entity['name']; + } + + if ('constructor' in entity && 'name' in entity.constructor) { + return entity['constructor']['name']; + } + + return `${entity}`; +}; + +export function getProviderName( + entity: EntityTarget, + name: string +) { + const entityName = getEntityName(entity); + return `${upperFirstLetter(entityName)}${name}`; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts new file mode 100644 index 00000000..4cba8fcf --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts @@ -0,0 +1,120 @@ +import { + InternalServerErrorException, + CallHandler, + ExecutionContext, + Inject, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { catchError, Observable, throwError } from 'rxjs'; +import { ObjectLiteral } from '../../../types'; +// import { QueryFailedError, Repository } from 'typeorm'; +// import { DriverUtils } from 'typeorm/driver/DriverUtils'; +// +// import { +// CONTROL_OPTIONS_TOKEN, +// CURRENT_ENTITY_REPOSITORY, +// } from '../../constants'; +// import { ConfigParam, Entity, ValidateQueryError } from '../../types'; +// import { +// MysqlError, +// MysqlErrorCode, +// PostgresError, +// PostgresErrorCode, +// } from '../../helper'; +// import { HttpException } from '@nestjs/common'; + +@Injectable() +export class ErrorInterceptors + implements NestInterceptor +{ + // @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; + // @Inject(CONTROL_OPTIONS_TOKEN) private config!: ConfigParam; + + intercept( + context: ExecutionContext, + next: CallHandler + ): Observable | Promise> { + return next.handle(); + // return next.handle().pipe( + // catchError((error) => { + // if (error instanceof QueryFailedError) { + // return throwError(() => this.prepareDataBaseError(error)); + // } + // + // if (error instanceof HttpException) { + // return throwError(() => error); + // } + // + // const errorObject: ValidateQueryError = { + // code: 'internal_error', + // message: this.config.debug ? error.message : 'Internal Server Error', + // path: [], + // }; + // const descriptionOrOptions = this.config.debug ? error : undefined; + // return throwError( + // () => + // new InternalServerErrorException( + // [errorObject], + // descriptionOrOptions + // ) + // ); + // }) + // ); + } + + // private prepareDataBaseError(error: QueryFailedError): HttpException { + // const errorObject: ValidateQueryError = { + // code: 'internal_error', + // message: this.config.debug ? error.message : 'Internal Server Error', + // path: [], + // }; + // + // if (DriverUtils.isMySQLFamily(this.repository.manager.connection.driver)) { + // const { errorCode, errorMsg } = this.prepareMysqlError(error.driverError); + // if (MysqlError[errorCode]) { + // return MysqlError[errorCode](this.repository.metadata, errorMsg); + // } + // } + // + // if ( + // DriverUtils.isPostgresFamily(this.repository.manager.connection.driver) + // ) { + // const { errorCode, errorMsg, detail } = this.preparePostgresError( + // error.driverError + // ); + // + // if (PostgresError[errorCode]) { + // return PostgresError[errorCode]( + // this.repository.metadata, + // errorMsg, + // detail + // ); + // } + // } + // + // return new InternalServerErrorException([errorObject]); + // } + // + // private prepareMysqlError(error: any): { + // errorCode: MysqlErrorCode; + // errorMsg: string; + // } { + // return { + // errorCode: error.errno, + // errorMsg: error.message, + // }; + // } + // + // private preparePostgresError(error: any): { + // errorCode: PostgresErrorCode; + // errorMsg: string; + // detail: string; + // } { + // return { + // errorCode: error.code, + // errorMsg: error.message, + // detail: error.detail, + // }; + // } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts new file mode 100644 index 00000000..b6030081 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts @@ -0,0 +1,2 @@ +export * from './error.interceptors'; +export * from './log-time.interceptors'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts new file mode 100644 index 00000000..2b6b2797 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts @@ -0,0 +1,30 @@ +import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; +import { map, Observable } from 'rxjs'; +import { ObjectLiteral } from '../../../types'; + +// import { Entity, ResourceObject } from '../../types'; + +export class LogTimeInterceptors + implements NestInterceptor +{ + intercept( + context: ExecutionContext, + // next: CallHandler> + next: CallHandler + ): Observable | Promise> { + const now = Date.now(); + return next.handle(); + // .pipe( + // map((r) => { + // const response = context.switchToHttp().getResponse(); + // const time = Date.now() - now; + // response.setHeader('x-response-time', time); + // if (r && r.meta) { + // r.meta['time'] = time; + // } + // + // return r; + // }) + // ); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts new file mode 100644 index 00000000..7d4ea222 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts @@ -0,0 +1,84 @@ +import { DynamicModule } from '@nestjs/common'; + +import { MixinOptions, DecoratorOptions } from './types'; +import { createController } from './helper'; +import { + JSON_API_DECORATOR_OPTIONS, + CONTROL_OPTIONS_TOKEN, + JSON_API_MODULE_POSTFIX, + CURRENT_ENTITY, + FIND_ONE_ROW_ENTITY, + CHECK_RELATION_NAME, +} from '../../constants'; +import { ConfigParam, RequiredFromPartial } from '../../types'; +import { MicroOrmParam } from '../micro-orm'; +import { TypeOrmParam } from '../type-orm'; +import { bindController, getProviderName, nameIt } from './helper'; +import { + ZodInputQuerySchema, + ZodPostSchema, + ZodQuerySchema, + ZodPatchSchema, + ZodInputPatchRelationshipSchema, + ZodInputPostRelationshipSchema, +} from './factory'; + +export class MixinModule { + static forRoot(options: MixinOptions): DynamicModule { + const { entity, controller, imports, ormModule } = options; + const controllerClass = createController(entity, controller); + + const decoratorOptions: DecoratorOptions = + Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, controllerClass) || {}; + + const moduleConfig: RequiredFromPartial< + ConfigParam & (MicroOrmParam | TypeOrmParam) + > = { + ...options.config, + ...decoratorOptions, + }; + + bindController(controllerClass, entity, moduleConfig); + const optionProvider = { + provide: CONTROL_OPTIONS_TOKEN, + useValue: moduleConfig, + }; + + const currentEntityProvider = { + provide: CURRENT_ENTITY, + useValue: entity, + }; + + const findOneRowEntityProvider = { + provide: FIND_ONE_ROW_ENTITY, + useValue: undefined, + }; + + const checkRelationNameProvider = { + provide: CHECK_RELATION_NAME, + useValue: undefined, + }; + + return { + module: nameIt( + getProviderName(entity, JSON_API_MODULE_POSTFIX), + MixinModule + ), + controllers: [controllerClass], + providers: [ + optionProvider, + currentEntityProvider, + findOneRowEntityProvider, + checkRelationNameProvider, + ...ormModule.getUtilProviders(entity), + ZodInputQuerySchema(), + ZodQuerySchema(), + ZodPatchSchema(), + ZodPostSchema(), + ZodInputPatchRelationshipSchema, + ZodInputPostRelationshipSchema, + ], + imports: imports, + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts new file mode 100644 index 00000000..a8e52f7d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts @@ -0,0 +1,55 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundException } from '@nestjs/common'; +import { CheckItemEntityPipe } from './check-item-entity.pipe'; +import { CURRENT_ENTITY, FIND_ONE_ROW_ENTITY } from '../../../../constants'; +import { EntityTarget } from 'typeorm/common/EntityTarget'; + +describe('CheckItemEntityPipe', () => { + let pipe: CheckItemEntityPipe; + let mockFindOneRowEntity: jest.Mock; + let mockEntityTarget: EntityTarget; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CheckItemEntityPipe, + { provide: CURRENT_ENTITY, useValue: {} }, + { provide: FIND_ONE_ROW_ENTITY, useValue: jest.fn() }, + ], + }).compile(); + + pipe = module.get>(CheckItemEntityPipe); + mockEntityTarget = module.get>(CURRENT_ENTITY); + mockFindOneRowEntity = module.get(FIND_ONE_ROW_ENTITY); + }); + + it('should call findOneRowEntity and return the entity', async () => { + const mockEntity = { id: 1, name: 'Test Entity' }; + const mockValue = 1; + + mockFindOneRowEntity.mockResolvedValue(mockEntity); + const result = await pipe.transform(mockValue); + + expect(mockFindOneRowEntity).toHaveBeenCalledTimes(1); + expect(mockFindOneRowEntity).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + + expect(result).toBe(mockValue); + }); + + it('should throw a NotFoundException if no entity is found', async () => { + const mockValue = 1; + + mockFindOneRowEntity.mockResolvedValue(null); + + await expect(pipe.transform(mockValue)).rejects.toThrow(NotFoundException); + + expect(mockFindOneRowEntity).toHaveBeenCalledTimes(1); + expect(mockFindOneRowEntity).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts new file mode 100644 index 00000000..8244d3aa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts @@ -0,0 +1,34 @@ +import { Inject, NotFoundException, PipeTransform } from '@nestjs/common'; +import { ValidateQueryError } from '../../../../types'; +import { CURRENT_ENTITY, FIND_ONE_ROW_ENTITY } from '../../../../constants'; +import { EntityTarget, ObjectLiteral } from '../../../../types'; +import { FindOneRowEntity } from '../../types'; +import { getEntityName } from '../../helper'; + +export class CheckItemEntityPipe< + E extends ObjectLiteral, + I extends string | number +> implements PipeTransform> +{ + @Inject(CURRENT_ENTITY) private currentEntity!: EntityTarget; + @Inject(FIND_ONE_ROW_ENTITY) private findOneRowEntity!: + | FindOneRowEntity + | undefined; + async transform(value: I): Promise { + if (!this.findOneRowEntity || typeof this.findOneRowEntity !== 'function') + return value; + + const result = await this.findOneRowEntity(this.currentEntity, value); + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${getEntityName( + this.currentEntity + )}' with id '${value}' does not exist`, + path: [], + }; + throw new NotFoundException([error]); + } + return value; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts new file mode 100644 index 00000000..256cdb57 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts @@ -0,0 +1 @@ +export * from './check-item-entity.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts new file mode 100644 index 00000000..6970b810 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts @@ -0,0 +1,46 @@ +import { INJECTABLE_WATERMARK } from '@nestjs/common/constants'; +import { PipeTransform } from '@nestjs/common/interfaces'; + +import { MixinOptions } from '../types'; +import { factoryMixin } from './index'; + +describe('factoryMixin', () => { + class TestEntity {} + + class TestPipe implements PipeTransform { + name = 'TestPipe'; + + transform(value: any) { + return value; + } + } + + function isInjectable(cls: any): boolean { + return Reflect.getMetadata(INJECTABLE_WATERMARK, cls); + } + + it('should return a pipe class with a new name', () => { + const pipeClass = factoryMixin( + TestEntity as MixinOptions['entity'], + TestPipe + ); + expect(pipeClass.name).toBe('TestEntityTestPipe'); + }); + + it('should return a pipe class that is Injectable', () => { + const pipeClass = factoryMixin( + TestEntity as MixinOptions['entity'], + TestPipe + ); + expect(isInjectable(pipeClass)).toBe(true); + }); + + it('should preserve the behavior of the original pipe', () => { + const pipeClass = factoryMixin( + TestEntity as MixinOptions['entity'], + TestPipe + ); + const instance = new pipeClass(); + expect(instance.transform('testValue', {} as any)).toBe('testValue'); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts new file mode 100644 index 00000000..ee2bf865 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts @@ -0,0 +1,89 @@ +import { Injectable, ParseIntPipe } from '@nestjs/common'; +import { upperFirstLetter } from '@klerick/json-api-nestjs-shared'; + +import { PipeMixin } from '../../../types'; +import { MixinOptions } from '../types'; +import { nameIt } from '../helper'; + +import { QueryInputPipe } from './query-input'; +import { QueryPipe } from './query'; +import { QueryFiledInIncludePipe } from './query-filed-on-include'; +import { QueryCheckSelectField } from './query-check-select-field'; +import { CheckItemEntityPipe } from './check-item-entity'; +import { PostInputPipe } from './post-input'; +import { PatchInputPipe } from './patch-input'; +import { ParseRelationshipNamePipe } from './parse-relationship-name'; +import { PostRelationshipPipe } from './post-relationship'; +import { PatchRelationshipPipe } from './patch-relationship'; + +export function factoryMixin(entity: MixinOptions['entity'], pipe: PipeMixin) { + const entityName = entity.name; + + const pipeClass = nameIt( + `${upperFirstLetter(entityName)}${pipe.name}`, + pipe + ) as PipeMixin; + + Injectable()(pipeClass); + + return pipeClass; +} + +export function queryInputMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, QueryInputPipe); +} + +export function queryMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, QueryPipe); +} + +export function queryFiledInIncludeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, QueryFiledInIncludePipe); +} + +export function queryCheckSelectFieldMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, QueryCheckSelectField); +} + +export function idPipeMixin( + entity: MixinOptions['entity'], + config?: MixinOptions['config'] +): PipeMixin { + return config && config.pipeForId ? config.pipeForId : (ParseIntPipe as any); +} + +export function checkItemEntityPipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, CheckItemEntityPipe); +} + +export function postInputPipeMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, PostInputPipe); +} + +export function patchInputPipeMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, PatchInputPipe); +} + +export function postRelationshipPipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, PostRelationshipPipe); +} + +export function patchRelationshipPipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, PatchRelationshipPipe); +} + +export function parseRelationshipNamePipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, ParseRelationshipNamePipe); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts new file mode 100644 index 00000000..62c4518d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts @@ -0,0 +1 @@ +export * from './parse-relationship-name.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts new file mode 100644 index 00000000..2709180b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts @@ -0,0 +1,58 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UnprocessableEntityException } from '@nestjs/common'; +import { ParseRelationshipNamePipe } from './parse-relationship-name.pipe'; +import { CURRENT_ENTITY, CHECK_RELATION_NAME } from '../../../../constants'; +import { EntityTarget } from 'typeorm/common/EntityTarget'; + +describe('CheckItemEntityPipe', () => { + let pipe: ParseRelationshipNamePipe; + let checkRelationNameMock: jest.Mock; + let mockEntityTarget: EntityTarget; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ParseRelationshipNamePipe, + { provide: CURRENT_ENTITY, useValue: {} }, + { provide: CHECK_RELATION_NAME, useValue: jest.fn() }, + ], + }).compile(); + + pipe = module.get>( + ParseRelationshipNamePipe + ); + mockEntityTarget = module.get>(CURRENT_ENTITY); + checkRelationNameMock = module.get(CHECK_RELATION_NAME); + }); + + it('should call findOneRowEntity and return the entity', async () => { + const mockValue = 'name'; + + checkRelationNameMock.mockReturnValueOnce(true); + const result = await pipe.transform(mockValue); + + expect(checkRelationNameMock).toHaveBeenCalledTimes(1); + expect(checkRelationNameMock).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + + expect(result).toBe(mockValue); + }); + + it('should throw a UnprocessableEntityException if no entity is found', async () => { + const mockValue = 'name'; + + checkRelationNameMock.mockReturnValueOnce(false); + + expect(() => pipe.transform(mockValue)).toThrow( + UnprocessableEntityException + ); + + expect(checkRelationNameMock).toHaveBeenCalledTimes(1); + expect(checkRelationNameMock).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts new file mode 100644 index 00000000..2224ec0b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts @@ -0,0 +1,38 @@ +import { + PipeTransform, + UnprocessableEntityException, + Inject, +} from '@nestjs/common'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { ValidateQueryError } from '../../../../types'; +import { CHECK_RELATION_NAME, CURRENT_ENTITY } from '../../../../constants'; +import { EntityTarget, ObjectLiteral } from '../../../../types'; +import { CheckRelationNme } from '../../types'; +import { getEntityName } from '../../helper'; + +export class ParseRelationshipNamePipe< + E extends ObjectLiteral, + I extends EntityRelation +> implements PipeTransform +{ + @Inject(CURRENT_ENTITY) private currentEntity!: EntityTarget; + @Inject(CHECK_RELATION_NAME) private checkRelationName!: CheckRelationNme; + + transform(value: string): I { + if (!this.checkRelationName || typeof this.checkRelationName !== 'function') + return value as I; + + const result = this.checkRelationName(this.currentEntity, value); + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Relation '${value}' does not exist in resource '${getEntityName( + this.currentEntity + )}'`, + path: [], + }; + throw new UnprocessableEntityException([error]); + } + return value as I; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts new file mode 100644 index 00000000..6d849c7a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts @@ -0,0 +1 @@ +export * from './patch-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts new file mode 100644 index 00000000..7e54641f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts @@ -0,0 +1,69 @@ +import { ZodError } from 'zod'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; + +import { PatchInputPipe } from './patch-input.pipe'; +import { PostData, ZodPost } from '../../zod'; +import { JSONValue } from '../../types'; +import { ZOD_PATCH_SCHEMA } from '../../../../constants'; + +type MockEntity = { id: number; name: string }; + +describe('PostInputPipe', () => { + let pipe: PatchInputPipe; + let mockSchema: ZodPost; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PatchInputPipe, + { + provide: ZOD_PATCH_SCHEMA, + useValue: { + parse: jest.fn(), + }, + }, + ], + }).compile(); + + pipe = module.get>(PatchInputPipe); + mockSchema = module.get>(ZOD_PATCH_SCHEMA); + }); + + it('should transform JSONValue to PostData on success', () => { + const input: JSONValue = { key: 'value' } as any; + const expectedData: PostData = { id: 1, key: 'value' } as any; + + jest + .spyOn(mockSchema, 'parse') + .mockReturnValue({ data: expectedData } as any); + + expect(pipe.transform(input)).toEqual(expectedData); + expect(mockSchema.parse).toHaveBeenCalledWith(input, { + errorMap: expect.any(Function), + }); + }); + + it('should throw BadRequestException if ZodError occurs', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new ZodError([]); + }); + + expect(() => pipe.transform(input)).toThrow(BadRequestException); + }); + + it('should throw InternalServerErrorException for non-Zod errors', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new Error('Unexpected Error'); + }); + + expect(() => pipe.transform(input)).toThrow(InternalServerErrorException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts new file mode 100644 index 00000000..3fd108af --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts @@ -0,0 +1,33 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; + +import { JSONValue } from '../../types'; +import { PatchData, ZodPatch } from '../../zod'; +import { ZOD_PATCH_SCHEMA } from '../../../../constants'; +import { ObjectLiteral } from '../../../../types'; + +export class PatchInputPipe + implements PipeTransform> +{ + @Inject(ZOD_PATCH_SCHEMA) + private zodInputPatchSchema!: ZodPatch; + transform(value: JSONValue): PatchData { + try { + return this.zodInputPatchSchema.parse(value, { + errorMap: errorMap, + })['data'] as PatchData; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts new file mode 100644 index 00000000..99ef16e8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts @@ -0,0 +1 @@ +export * from './patch-relationship.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts new file mode 100644 index 00000000..8c591419 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts @@ -0,0 +1,84 @@ +import { IMemoryDb } from 'pg-mem'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + InternalServerErrorException, + BadRequestException, +} from '@nestjs/common'; + +import { ZOD_PATCH_RELATIONSHIP_SCHEMA } from '../../../../constants'; + +import { PatchRelationshipPipe } from './patch-relationship.pipe'; +import { ZodPatchRelationship } from '../../zod'; +import { ZodError } from 'zod'; + +describe('PatchInputPipe', () => { + let patchRelationshipPipe: PatchRelationshipPipe; + let zodInputPatchRelationshipSchema: ZodPatchRelationship; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, + useValue: { + parse() {}, + }, + }, + PatchRelationshipPipe, + ], + }).compile(); + + patchRelationshipPipe = module.get( + PatchRelationshipPipe + ); + zodInputPatchRelationshipSchema = module.get( + ZOD_PATCH_RELATIONSHIP_SCHEMA + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('It should be ok', () => { + const data = { + some: 'data', + }; + const check = { + data, + }; + jest + .spyOn(zodInputPatchRelationshipSchema, 'parse') + .mockImplementationOnce(() => check as any); + expect(patchRelationshipPipe.transform(check)).toEqual(data); + }); + + it('Should be not ok', () => { + jest + .spyOn(zodInputPatchRelationshipSchema, 'parse') + .mockImplementationOnce(() => { + throw new ZodError([]); + }); + expect.assertions(1); + try { + patchRelationshipPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + it('Should be 500', () => { + jest + .spyOn(zodInputPatchRelationshipSchema, 'parse') + .mockImplementationOnce(() => { + throw new Error('Error mock'); + }); + expect.assertions(1); + + try { + patchRelationshipPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(InternalServerErrorException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts new file mode 100644 index 00000000..bf23ae36 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts @@ -0,0 +1,32 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; + +import { JSONValue } from '../../types'; +import { PatchRelationshipData, ZodPatchRelationship } from '../../zod'; +import { ZOD_PATCH_RELATIONSHIP_SCHEMA } from '../../../../constants'; + +export class PatchRelationshipPipe + implements PipeTransform +{ + @Inject(ZOD_PATCH_RELATIONSHIP_SCHEMA) + private zodInputPatchRelationshipSchema!: ZodPatchRelationship; + transform(value: JSONValue): PatchRelationshipData { + try { + return this.zodInputPatchRelationshipSchema.parse(value, { + errorMap: errorMap, + })['data']; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts new file mode 100644 index 00000000..58efc255 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts @@ -0,0 +1 @@ +export * from './post-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts new file mode 100644 index 00000000..01481275 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts @@ -0,0 +1,69 @@ +import { ZodError } from 'zod'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; + +import { PostInputPipe } from './post-input.pipe'; +import { PostData, ZodPost } from '../../zod'; +import { JSONValue } from '../../types'; +import { ZOD_POST_SCHEMA } from '../../../../constants'; + +type MockEntity = { id: number; name: string }; + +describe('PostInputPipe', () => { + let pipe: PostInputPipe; + let mockSchema: ZodPost; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PostInputPipe, + { + provide: ZOD_POST_SCHEMA, + useValue: { + parse: jest.fn(), + }, + }, + ], + }).compile(); + + pipe = module.get>(PostInputPipe); + mockSchema = module.get>(ZOD_POST_SCHEMA); + }); + + it('should transform JSONValue to PostData on success', () => { + const input: JSONValue = { key: 'value' } as any; + const expectedData: PostData = { id: 1, key: 'value' } as any; + + jest + .spyOn(mockSchema, 'parse') + .mockReturnValue({ data: expectedData } as any); + + expect(pipe.transform(input)).toEqual(expectedData); + expect(mockSchema.parse).toHaveBeenCalledWith(input, { + errorMap: expect.any(Function), + }); + }); + + it('should throw BadRequestException if ZodError occurs', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new ZodError([]); + }); + + expect(() => pipe.transform(input)).toThrow(BadRequestException); + }); + + it('should throw InternalServerErrorException for non-Zod errors', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new Error('Unexpected Error'); + }); + + expect(() => pipe.transform(input)).toThrow(InternalServerErrorException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts new file mode 100644 index 00000000..5059b8ce --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts @@ -0,0 +1,32 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; + +import { PostData, ZodPost } from '../../zod'; +import { ZOD_POST_SCHEMA } from '../../../../constants'; +import { ObjectLiteral } from '../../../../types'; +import { JSONValue } from '../../types'; + +export class PostInputPipe + implements PipeTransform> +{ + @Inject(ZOD_POST_SCHEMA) private zodInputPostSchema!: ZodPost; + transform(value: JSONValue): PostData { + try { + return this.zodInputPostSchema.parse(value, { + errorMap: errorMap, + })['data'] as PostData; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts new file mode 100644 index 00000000..70457a78 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts @@ -0,0 +1 @@ +export * from './post-relationship.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts new file mode 100644 index 00000000..8833734d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts @@ -0,0 +1,83 @@ +import { IMemoryDb } from 'pg-mem'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { + InternalServerErrorException, + BadRequestException, +} from '@nestjs/common'; +import { ZOD_POST_RELATIONSHIP_SCHEMA } from '../../../../constants'; + +import { PostRelationshipPipe } from './post-relationship.pipe'; +import { ZodPostRelationship } from '../../zod'; +import { ZodError } from 'zod'; + +describe('PostInputPipe', () => { + let postRelationshipPipe: PostRelationshipPipe; + let zodInputPostRelationshipSchema: ZodPostRelationship; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ZOD_POST_RELATIONSHIP_SCHEMA, + useValue: { + parse() {}, + }, + }, + PostRelationshipPipe, + ], + }).compile(); + + postRelationshipPipe = + module.get(PostRelationshipPipe); + zodInputPostRelationshipSchema = module.get( + ZOD_POST_RELATIONSHIP_SCHEMA + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('It should be ok', () => { + const data = { + some: 'data', + }; + const check = { + data, + }; + jest + .spyOn(zodInputPostRelationshipSchema, 'parse') + .mockImplementationOnce(() => check as any); + expect(postRelationshipPipe.transform(check)).toEqual(data); + }); + + it('Should be not ok', () => { + jest + .spyOn(zodInputPostRelationshipSchema, 'parse') + .mockImplementationOnce(() => { + throw new ZodError([]); + }); + expect.assertions(1); + try { + postRelationshipPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + it('Should be 500', () => { + jest + .spyOn(zodInputPostRelationshipSchema, 'parse') + .mockImplementationOnce(() => { + throw new Error('Error mock'); + }); + expect.assertions(1); + + try { + postRelationshipPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(InternalServerErrorException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts new file mode 100644 index 00000000..4a9b5820 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts @@ -0,0 +1,32 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; + +import { JSONValue } from '../../types'; +import { PostRelationshipData, ZodPostRelationship } from '../../zod'; +import { ZOD_POST_RELATIONSHIP_SCHEMA } from '../../../../constants'; + +export class PostRelationshipPipe + implements PipeTransform +{ + @Inject(ZOD_POST_RELATIONSHIP_SCHEMA) + private zodInputPostRelationshipSchema!: ZodPostRelationship; + transform(value: JSONValue): PostRelationshipData { + try { + return this.zodInputPostRelationshipSchema.parse(value, { + errorMap: errorMap, + })['data']; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts new file mode 100644 index 00000000..7ba4ed4e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts @@ -0,0 +1 @@ +export * from './query-check-select-field'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts new file mode 100644 index 00000000..bd87447c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts @@ -0,0 +1,73 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException } from '@nestjs/common'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; + +import { QueryCheckSelectField } from './query-check-select-field'; +import { Users } from '../../../../mock-utils'; +import { CONTROL_OPTIONS_TOKEN } from '../../../../constants'; +import { Query } from '../../zod'; +import { ConfigParam, ObjectLiteral } from '../../../../types'; + +function getDefaultQuery() { + const filter = { + relation: null, + target: null, + }; + const defaultQuery: Query = { + [QueryField.filter]: filter, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: 1, + number: 1, + }, + }; + + return defaultQuery; +} + +describe('QueryCheckSelectField', () => { + let queryCheckSelectField: QueryCheckSelectField; + let configParam: ConfigParam; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + QueryCheckSelectField, + ], + }).compile(); + + queryCheckSelectField = module.get>( + QueryCheckSelectField + ); + configParam = module.get(CONTROL_OPTIONS_TOKEN); + }); + + it('Is valid', () => { + const query = getDefaultQuery(); + expect(queryCheckSelectField.transform(query)).toEqual(query); + }); + + it('Is invalid', () => { + const query = getDefaultQuery(); + jest.mocked(configParam).requiredSelectField = true; + expect.assertions(1); + try { + queryCheckSelectField.transform(query); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts new file mode 100644 index 00000000..7b98707f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts @@ -0,0 +1,25 @@ +import { BadRequestException, Inject, PipeTransform } from '@nestjs/common'; +import { CONTROL_OPTIONS_TOKEN } from '../../../../constants'; +import { + ConfigParam, + ObjectLiteral, + ValidateQueryError, +} from '../../../../types'; +import { Query } from '../../zod'; + +export class QueryCheckSelectField + implements PipeTransform, Query> +{ + @Inject(CONTROL_OPTIONS_TOKEN) private configParam!: ConfigParam; + transform(value: Query): Query { + if (this.configParam.requiredSelectField && value.fields === null) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Fields params in query is required'`, + path: ['fields'], + }; + throw new BadRequestException([error]); + } + return value; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts new file mode 100644 index 00000000..82b1b95f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts @@ -0,0 +1 @@ +export * from './query-filed-in-include.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts new file mode 100644 index 00000000..04bdb9c4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts @@ -0,0 +1,121 @@ +import { BadRequestException } from '@nestjs/common'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { QueryFiledInIncludePipe } from './query-filed-in-include.pipe'; +import { Users } from '../../../../mock-utils'; +import { Query } from '../../zod'; + +describe('QueryFiledInIncludePipe', () => { + let queryFiledInIncludePipe: QueryFiledInIncludePipe; + + beforeAll(() => { + queryFiledInIncludePipe = new QueryFiledInIncludePipe(); + }); + + it('Should be ok', () => { + const check: Query = { + [QueryField.fields]: { + roles: ['id'], + }, + [QueryField.include]: ['roles'], + [QueryField.filter]: { + target: null, + relation: null, + }, + [QueryField.sort]: null, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + + const check2: Query = { + [QueryField.fields]: null, + [QueryField.include]: ['roles'], + [QueryField.filter]: { + target: null, + relation: { + roles: { name: { eq: 'test' } }, + }, + }, + [QueryField.sort]: null, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + + const result = queryFiledInIncludePipe.transform(check); + expect(result).toEqual(check); + const result2 = queryFiledInIncludePipe.transform(check2); + expect(result2).toEqual(check2); + }); + + it('Should be not ok', () => { + const check: Query = { + [QueryField.fields]: { + roles: ['id'], + }, + [QueryField.include]: null, + [QueryField.filter]: { + target: null, + relation: null, + }, + [QueryField.sort]: null, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + const check2: Query = { + [QueryField.fields]: { + roles: ['id'], + }, + [QueryField.include]: null, + [QueryField.filter]: { + target: null, + relation: null, + }, + [QueryField.sort]: { + addresses: { + id: 'ASC', + }, + }, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + + const check3: Query = { + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.filter]: { + target: null, + relation: { + roles: { name: { eq: 'test' } }, + }, + }, + [QueryField.sort]: null, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + expect.assertions(3); + try { + queryFiledInIncludePipe.transform(check); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + try { + queryFiledInIncludePipe.transform(check2); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + try { + queryFiledInIncludePipe.transform(check3); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts new file mode 100644 index 00000000..dc27f7f4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts @@ -0,0 +1,70 @@ +import { BadRequestException, PipeTransform } from '@nestjs/common'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { Query } from '../../zod'; + +export class QueryFiledInIncludePipe + implements PipeTransform, Query> +{ + transform(value: Query): Query { + const errors: ValidateQueryError[] = []; + + const { fields, include, sort, filter } = value; + const includeSet = new Set(); + + if (include) { + include.reduce((acum, item) => acum.add(item), includeSet); + } + + if (filter) { + const { relation } = filter; + if (relation) { + const filterRelationFields = ObjectTyped.keys(relation); + const filterFieldsErrors = filterRelationFields + .filter((i) => !includeSet.has(i.toString())) + .map((i) => ({ + code: 'invalid_intersection_types', + message: `Add '${i.toString()}' to query param 'include'`, + path: ['filter', 'relation', i.toString()], + })); + + errors.push(...filterFieldsErrors); + } + } + + if (fields) { + const { target: targetResourceFields, ...relationFields } = fields; + const selectRelationFields = ObjectTyped.keys(relationFields); + const fieldsErrors = selectRelationFields + .filter((i) => !includeSet.has(i.toString())) + .map((i) => ({ + code: 'invalid_intersection_types', + message: `Add '${i.toString()}' to query param 'include'`, + path: ['fields'], + })); + + errors.push(...fieldsErrors); + } + + if (sort) { + const { target: targetResourceSorts, ...relationSorts } = sort; + const selectRelationFields = ObjectTyped.keys(relationSorts); + const fieldsErrors = selectRelationFields + .filter((i) => !includeSet.has(i.toString())) + .map((i) => ({ + code: 'invalid_intersection_types', + message: `Add '${i.toString()}' to query param 'include'`, + path: ['sort'], + })); + + errors.push(...fieldsErrors); + } + + if (errors.length > 0) { + throw new BadRequestException(errors); + } + + return value; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts new file mode 100644 index 00000000..7422bf5a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts @@ -0,0 +1 @@ +export * from './query-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts new file mode 100644 index 00000000..c35bebab --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts @@ -0,0 +1,65 @@ +import { QueryInputPipe } from './query-input.pipe'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ZodError } from 'zod'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; + +import { ZOD_INPUT_QUERY_SCHEMA } from '../../../../constants'; + +class MockZodInputQuery { + parse(value: unknown, options?: unknown) { + return value; + } +} + +describe('QueryInputPipe', () => { + let pipe: QueryInputPipe; + let zodSchemaMock: MockZodInputQuery; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QueryInputPipe, + { provide: ZOD_INPUT_QUERY_SCHEMA, useClass: MockZodInputQuery }, + ], + }).compile(); + + pipe = module.get>(QueryInputPipe); + zodSchemaMock = module.get(ZOD_INPUT_QUERY_SCHEMA); + }); + + it('should parse the input successfully', () => { + const input = { key: 'value' }; + jest.spyOn(zodSchemaMock, 'parse').mockReturnValue(input); + + const result = pipe.transform(input); + expect(result).toBe(input); + expect(zodSchemaMock.parse).toHaveBeenCalledWith(input, { + errorMap: expect.any(Function), + }); + }); + + it('should throw a BadRequestException when ZodError occurs', () => { + const input = { invalid: 'data' }; + const mockZodError = new ZodError([]); + + jest.spyOn(zodSchemaMock, 'parse').mockImplementation(() => { + throw mockZodError; + }); + + expect(() => pipe.transform(input)).toThrow(BadRequestException); + }); + + it('should throw an InternalServerErrorException for non-ZodError exceptions', () => { + const input = { key: 'value' }; + const mockError = new Error('Unexpected error'); + + jest.spyOn(zodSchemaMock, 'parse').mockImplementation(() => { + throw mockError; + }); + + expect(() => pipe.transform(input)).toThrow(InternalServerErrorException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts new file mode 100644 index 00000000..c1129549 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts @@ -0,0 +1,33 @@ +import { + Inject, + PipeTransform, + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; +import { ZOD_INPUT_QUERY_SCHEMA } from '../../../../constants'; +import { ZodInputQuery, InputQuery } from '../../zod'; +import { JSONValue } from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +export class QueryInputPipe + implements PipeTransform> +{ + @Inject(ZOD_INPUT_QUERY_SCHEMA) + private zodInputQuerySchema!: ZodInputQuery; + + transform(value: JSONValue): InputQuery { + try { + return this.zodInputQuerySchema.parse(value, { + errorMap: errorMap, + }); + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts new file mode 100644 index 00000000..a8853ae0 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts @@ -0,0 +1 @@ +export * from './query.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts new file mode 100644 index 00000000..cfb58c24 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts @@ -0,0 +1,107 @@ +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ZodError } from 'zod'; + +import { QueryPipe } from './query.pipe'; +import { ASC, ZOD_QUERY_SCHEMA } from '../../../../constants'; +import { ZodQuery, InputQuery, Query } from '../../zod'; +import { FilterOperand, QueryField } from '@klerick/json-api-nestjs-shared'; + +type MockEntity = { id: number; name: string }; + +describe('QueryPipe', () => { + let queryPipe: QueryPipe; + let zodQuerySchemaMock: ZodQuery; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QueryPipe, + { + provide: ZOD_QUERY_SCHEMA, + useValue: { + parse: jest.fn(), + }, + }, + ], + }).compile(); + + queryPipe = module.get>(QueryPipe); + zodQuerySchemaMock = module.get>(ZOD_QUERY_SCHEMA); + }); + + it('should parse the query successfully using the zod schema', () => { + const inputQuery: InputQuery = { + [QueryField.fields]: { + target: ['id', 'name'], + }, + [QueryField.filter]: { + relation: null, + target: { + id: { + [FilterOperand.eq]: '1', + }, + }, + }, + [QueryField.include]: null, + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + const parsedQuery: Query = { + [QueryField.fields]: { + target: ['id', 'name'], + }, + [QueryField.filter]: { + relation: null, + target: { + id: { + eq: '1', + }, + }, + }, + [QueryField.include]: null, + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + + jest.spyOn(zodQuerySchemaMock, 'parse').mockReturnValue(parsedQuery); + + const result = queryPipe.transform(inputQuery); + + expect(result).toEqual(parsedQuery); + expect(zodQuerySchemaMock.parse).toHaveBeenCalledWith(inputQuery); + }); + + it('should throw BadRequestException if ZodError is thrown', () => { + const inputQuery = { + id: 1, + name: 'Invalid', + } as unknown as InputQuery; + const zodError = new ZodError([]); + + jest.spyOn(zodQuerySchemaMock, 'parse').mockImplementation(() => { + throw zodError; + }); + + expect(() => queryPipe.transform(inputQuery)).toThrow(BadRequestException); + }); + + it('should throw InternalServerErrorException if an unknown error is thrown', () => { + const inputQuery = { + id: 1, + name: 'Invalid', + } as unknown as InputQuery; + const unexpectedError = new Error('Unexpected error'); + + jest.spyOn(zodQuerySchemaMock, 'parse').mockImplementation(() => { + throw unexpectedError; + }); + + expect(() => queryPipe.transform(inputQuery)).toThrow( + InternalServerErrorException + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts new file mode 100644 index 00000000..d4c5d359 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts @@ -0,0 +1,30 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; + +import { ZOD_QUERY_SCHEMA } from '../../../../constants'; +import { ZodQuery, Query, InputQuery } from '../../zod'; +import { ObjectLiteral } from '../../../../types'; + +export class QueryPipe + implements PipeTransform, Query> +{ + @Inject(ZOD_QUERY_SCHEMA) + private zodQuerySchema!: ZodQuery; + + transform(value: InputQuery): Query { + try { + return this.zodQuerySchema.parse(value); + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts new file mode 100644 index 00000000..eebff2fc --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts @@ -0,0 +1,47 @@ +import { PipeTransform, RequestMethod } from '@nestjs/common'; +import { Type } from '@nestjs/common/interfaces'; +import { PipeFabric } from './module.types'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { ObjectLiteral } from '../../../types'; + +export type MethodName = + | 'getAll' + | 'getOne' + | 'postOne' + | 'patchOne' + | 'getRelationship' + | 'deleteOne' + | 'deleteRelationship' + | 'postRelationship' + | 'patchRelationship'; + +type MapNameToTypeMethod = { + getAll: RequestMethod.GET; + getOne: RequestMethod.GET; + patchOne: RequestMethod.PATCH; + patchRelationship: RequestMethod.PATCH; + postOne: RequestMethod.POST; + postRelationship: RequestMethod.POST; + deleteOne: RequestMethod.DELETE; + deleteRelationship: RequestMethod.DELETE; + getRelationship: RequestMethod.GET; +}; + +export interface Binding { + path: string; + method: MapNameToTypeMethod[T]; + name: T; + implementation: JsonBaseController[T]; + parameters: { + decorator: ( + property?: string, + ...pipes: (Type | PipeTransform)[] + ) => ParameterDecorator; + property?: string; + mixins: PipeFabric[]; + }[]; +} + +export type BindingsConfig = { + [Key in MethodName]: Binding; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts new file mode 100644 index 00000000..4911b5dd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts @@ -0,0 +1,11 @@ +import { MethodName } from './binding.types'; + +import { RequiredFromPartial, ConfigParam } from '../../../types'; +import { MicroOrmParam } from '../../micro-orm'; +import { TypeOrmParam } from '../../type-orm'; + +export type DecoratorOptions = Partial< + { + allowMethod: Array; + } & RequiredFromPartial +>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts new file mode 100644 index 00000000..d3876db6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts @@ -0,0 +1,6 @@ +export * from './module.types'; +export * from './decorator-options.types'; +export * from './binding.types'; +export * from './utils'; +export * from './zod-types'; +export * from './orm-service.type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts new file mode 100644 index 00000000..768387c6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts @@ -0,0 +1,28 @@ +import { + AnyEntity, + EntityName, + NestImport, + NestController, + RequiredFromPartial, + ConfigParam, + PipeMixin, + ExtractNestType, + ResultModuleOptions, +} from '../../../types'; +import { MicroOrmParam } from '../../micro-orm'; +import { TypeOrmParam } from '../../type-orm'; + +type Controller = ExtractNestType; + +export interface MixinOptions { + entity: EntityName; + controller: Controller | undefined; + config: RequiredFromPartial; + imports: NestImport; + ormModule: ResultModuleOptions['type']; +} + +export type PipeFabric = >( + entity: Entity, + config?: MixinOptions['config'] +) => PipeMixin; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts new file mode 100644 index 00000000..b6a501ac --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts @@ -0,0 +1,55 @@ +import { EntityTarget, ObjectLiteral } from '../../../types'; + +import { + EntityRelation, + ResourceObject, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../zod'; + +export interface OrmService { + getAll(query: Query): Promise>; + getOne(id: number | string, query: QueryOne): Promise>; + deleteOne(id: number | string): Promise; + postOne(inputData: PostData): Promise>; + patchOne( + id: number | string, + inputData: PatchData + ): Promise>; + getRelationship>( + id: number | string, + rel: Rel + ): Promise>; + postRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise>; + deleteRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise; + patchRelationship>( + id: number | string, + rel: Rel, + input: PatchRelationshipData + ): Promise>; +} + +export type FindOneRowEntity = ( + entity: EntityTarget, + params: number | string +) => Promise; + +export type CheckRelationNme = ( + entity: EntityTarget, + params: string +) => boolean; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts new file mode 100644 index 00000000..821a02fb --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts @@ -0,0 +1,68 @@ +import { Type } from '@nestjs/common/interfaces'; + +import { + EntityField, + EntityRelation, + TypeOfArray, + EntityProps, +} from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral as Entity } from '../../../types'; + +export { EntityField, EntityProps, EntityRelation, TypeOfArray }; + +export type EntityPropsArray = { + [P in keyof T]: T[P] extends EntityField + ? IsArray extends true + ? P + : never + : never; +}[keyof T]; + +type UnionToIntersection = ( + U extends never ? never : (arg: U) => never +) extends (arg: infer I) => void + ? I + : never; + +export type UnionToTupleMain = UnionToIntersection< + T extends never ? never : (t: T) => T +> extends (_: never) => infer W + ? UnionToTupleMain, [...A, W]> + : A; + +export type UnionToTuple = UnionToTupleMain extends readonly [ + string, + ...string[] +] + ? UnionToTupleMain + : never; + +export type TypeCast = A extends T ? A : never; + +export type CastProps = K extends keyof E ? E[K] : never; + +export type TypeFromType = T extends Type ? A : never; + +export type ConcatStringArray = T extends [ + infer F extends string, + ...infer R extends string[] +] + ? `${F}${ConcatStringArray}` + : ''; + +export type Concat = ConcatStringArray< + [E, '.', F] +>; + +export type ValueOf = T[keyof T]; + +export type JSONValue = + | string + | number + | boolean + | null + | { [x: string]: JSONValue } + | Array; + +export type IsArray = [Extract] extends [never] ? false : true; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts new file mode 100644 index 00000000..cf413985 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts @@ -0,0 +1,141 @@ +import { Type } from '@nestjs/common'; +import { z } from 'zod'; + +import { + EntityProps, + EntityRelation, + UnionToTuple, + CastProps, + TypeOfArray, + EntityPropsArray, + IsArray, +} from './utils'; +import { + EntityTarget, + ObjectLiteral as Entity, + ObjectLiteral, +} from '../../../types'; + +export enum PropsNameResultField { + field = 'field', + relations = 'relations', +} + +export type ResultGetField = { + [PropsNameResultField.field]: TupleOfEntityProps; + [PropsNameResultField.relations]: TupleOfEntityRelation; +}; + +export type TupleOfEntityProps< + E, + Props = UnionToTuple> +> = Props extends readonly [string, ...string[]] ? Props : never; +export type TupleOfEntityRelation< + E, + Props = UnionToTuple> +> = Props extends readonly [string, ...string[]] ? Props : never; + +export type RelationTree = { + [K in keyof RelationType]: TypeOfArray extends Entity + ? ResultGetField>['field'] + : never; +}; + +export type RelationType = { + [K in EntityRelation]: Type>>; +}; + +export type ZodInfer any> = z.infer>; + +export type GetFieldForEntity = ( + entity: EntityTarget +) => ResultGetField; + +export type ZodParams< + E extends Entity, + P extends EntityProps, + I = string +> = { + entityFieldsStructure: ResultGetField; + entityRelationStructure: RelationTree; + propsArray: ArrayPropsForEntity; + propsType: AllFieldWithType; + typeId: TypeForId; + typeName: I; + fieldWithType: FieldWithType; + propsDb: PropsForField; + primaryColumn: P; + relationArrayProps: RelationPropsArray; + relationPopsName: RelationPropsTypeName; + primaryColumnType: RelationPrimaryColumnType; +}; + +export type PropsArray = { [K in EntityPropsArray]: true }; + +export type ArrayPropsForEntity = { + target: PropsArray; +} & { + [K in ResultGetField['relations'][number]]: PropsArray< + TypeOfArray> + >; +}; + +export enum TypeField { + array = 'array', + date = 'date', + number = 'number', + boolean = 'boolean', + string = 'string', + object = 'object', +} +export type TypeForId = Extract; + +export type FieldWithType = { + [K in EntityProps]: IsArray extends true + ? TypeField.array + : E[K] extends Date + ? TypeField.date + : E[K] extends number + ? TypeField.number + : E[K] extends boolean + ? TypeField.boolean + : E[K] extends object + ? TypeField.object + : TypeField.string; +}; + +export type AllFieldWithType = FieldWithType & { + [K in EntityRelation]: E[K] extends (infer U extends Entity)[] + ? FieldWithType + : E[K] extends Entity + ? FieldWithType + : never; +}; + +export type PropsForField = { + [K in EntityProps]: PropsFieldItem; +}; + +export type ColumnType = + | T + | typeof Number + | typeof Date + | typeof Boolean; + +export type PropsFieldItem = { + type: ColumnType; + isArray: boolean; + isNullable: boolean; +}; + +export type RelationPropsArray = { + [K in EntityRelation]: E[K] extends unknown[] ? true : false; +}; + +export type RelationPropsTypeName = { + [K in EntityRelation]: string; +}; + +export type RelationPrimaryColumnType = { + [K in EntityRelation]: TypeForId; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts new file mode 100644 index 00000000..6d2793fd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts @@ -0,0 +1,6 @@ +export * from './zod-input-query-schema'; +export * from './zod-query-schema'; +export * from './zod-input-post-schema'; +export * from './zod-input-patch-schema'; +export * from './zod-input-post-relationship-schema'; +export * from './zod-input-patch-relationship-schema'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts new file mode 100644 index 00000000..0328c8be --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts @@ -0,0 +1,42 @@ +import { zodPatchRelationship } from './index'; + +describe('zodPatchRelationship', () => { + const schema = zodPatchRelationship; + + it('should validate an object with nullable data matching zodData', () => { + const validData = { data: { id: '123', type: 'example' } }; + + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with null as the value of data', () => { + const validData = { data: null }; + + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with empty array as the value of data', () => { + const validData = { data: [] }; + + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with an array of objects matching zodData', () => { + const validData = { + data: [ + { id: '123', type: 'example' }, + { id: '456', type: 'example2' }, + ], + }; + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should throw an error for extra unknown properties', () => { + const invalidData = { + data: { id: '123', type: 'example' }, + extra: 'invalid', + }; + + expect(() => schema.parse(invalidData)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts new file mode 100644 index 00000000..de334b2d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +import { zodData } from '../zod-share'; + +export const zodPatchRelationship = z + .object({ + data: z.union([zodData().nullable(), zodData().array()]), + }) + .strict(); + +export type ZodPatchRelationship = typeof zodPatchRelationship; +export type PatchRelationship = z.infer; +export type PatchRelationshipData = PatchRelationship['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts new file mode 100644 index 00000000..b86972dd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts @@ -0,0 +1,220 @@ +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeField, + TypeForId, +} from '../../types'; +import { Users } from '../../../../mock-utils'; +import { zodPatch, PatchData } from './'; +import { ZodError } from 'zod'; + +const typeId: TypeForId = TypeField.number; +const typeName = 'Users'; +const fieldWithType: FieldWithType = { + id: TypeField.number, + login: TypeField.string, + firstName: TypeField.string, + testReal: TypeField.array, + testArrayNull: TypeField.array, + lastName: TypeField.string, + isActive: TypeField.boolean, + createdAt: TypeField.date, + testDate: TypeField.date, + updatedAt: TypeField.date, +}; +const propsDb: PropsForField = { + id: { type: 'number', isArray: false, isNullable: false }, + login: { type: 'string', isArray: false, isNullable: false }, + firstName: { type: 'string', isArray: false, isNullable: true }, + testReal: { type: 'number', isArray: true, isNullable: false }, + testArrayNull: { type: 'number', isArray: true, isNullable: true }, + lastName: { type: 'string', isArray: false, isNullable: true }, + isActive: { type: 'boolean', isArray: false, isNullable: true }, + createdAt: { type: 'date', isArray: false, isNullable: true }, + testDate: { type: 'date', isArray: false, isNullable: true }, + updatedAt: { type: 'date', isArray: false, isNullable: true }, +}; +const primaryColumn: EntityProps = 'id'; +const relationArrayProps: RelationPropsArray = { + roles: true, + comments: true, + notes: true, + addresses: false, + userGroup: false, + manager: false, +}; +const relationPopsName: RelationPropsTypeName = { + roles: 'Roles', + comments: 'Comments', + notes: 'Notes', + addresses: 'Addresses', + userGroup: 'UserGroups', + manager: 'Users', +}; +const primaryColumnType: RelationPrimaryColumnType = { + roles: TypeField.number, + userGroup: TypeField.number, + manager: TypeField.number, + addresses: TypeField.number, + comments: TypeField.number, + notes: TypeField.string, +}; +const schema = zodPatch( + typeId, + 'users', + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType +); + +describe('zodPatch', () => { + it('should be ok', () => { + const real = 123.123; + const date = new Date(); + const attributes = { + login: 'login', + testDate: date.toISOString(), + testReal: [`${real}`], + testArrayNull: null, + }; + const relationships = { + notes: { + data: [ + { + type: 'notes', + id: 'dsfsdf', + }, + ], + }, + }; + + const check = { + data: { + id: '1', + type: 'users', + attributes, + relationships, + }, + }; + const check2 = { + data: { + id: '1', + type: 'users', + attributes, + }, + }; + + const checkResult = { + data: { + id: '1', + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + relationships, + }, + }; + const checkResult2 = { + data: { + id: '1', + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + }, + }; + + const checkResult3 = { + data: { + id: '1', + type: 'users', + }, + }; + + expect(schema.parse(check)).toEqual(checkResult); + expect(schema.parse(check2)).toEqual(checkResult2); + expect(schema.parse(checkResult3)).toEqual(checkResult3); + }); + it('should be not ok', () => { + const check1 = {}; + const check2 = null; + const check3: unknown[] = []; + const check4 = ''; + const check5 = { + sdf: 'sdf', + }; + const check6 = { + data: {}, + }; + const check7 = { + data: { + type: 'users', + }, + }; + const check8 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + isActive: true, + }, + relationships: { + notes: [ + { + type: 'sdfsdf', + id: 'dsfsdf', + }, + ], + }, + }, + }; + const check9 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + id: 1, + }, + }, + }; + const check10 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + }, + }, + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + check10, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts new file mode 100644 index 00000000..b1444e11 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts @@ -0,0 +1,111 @@ +import { z, ZodObject, ZodOptional } from 'zod'; +import { + zodAttributes, + ZodAttributes, + zodId, + ZodId, + zodRelationships, + ZodRelationships, + zodType, + ZodType, +} from '../zod-share'; +import { ObjectLiteral } from '../../../../types'; +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeForId, +} from '../../types'; +import { ZodPost } from '../zod-input-post-schema'; + +type ZodPatchPatchShape = { + id: ZodId; + type: ZodType; + attributes: ZodOptional>; + relationships: ZodOptional>; +}; + +type ZodInputPatchSchema = ZodObject< + ZodPatchPatchShape, + 'strict' +>; + +type ZodInputPatchDataShape = { + data: ZodInputPatchSchema; +}; + +function getShape( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodInputPatchSchema { + const shape = { + id: zodId(typeId), + type: zodType(typeName), + attributes: zodAttributes( + fieldWithType, + propsDb, + primaryColumn, + true + ).optional(), + relationships: zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + true + ).optional(), + }; + + return z.object(shape).strict(); +} + +function zodDataShape( + shape: ZodInputPatchSchema +): ZodPatch { + return z + .object({ + data: shape, + }) + .strict(); +} + +export function zodPatch( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodPatch { + const shape = getShape( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + + return zodDataShape(shape); +} + +export type ZodPatch = ZodObject< + ZodInputPatchDataShape, + 'strict' +>; +export type PatchData< + E extends ObjectLiteral, + N extends string = string +> = z.infer>['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts new file mode 100644 index 00000000..9b084d58 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts @@ -0,0 +1,43 @@ +import { zodPostRelationship } from './index'; + +describe('zodPostRelationship', () => { + const schema = zodPostRelationship; + + it('should validate an object with a single valid data item', () => { + const validData = { data: { id: '1', type: 'example' } }; + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with a non-empty array of valid data items', () => { + const validData = { + data: [ + { id: '1', type: 'example1' }, + { id: '2', type: 'example2' }, + ], + }; + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should throw an error when data is an empty array', () => { + const invalidData = { data: [] }; + expect(() => schema.parse(invalidData)).toThrow(); + }); + + it('should throw an error when data is null', () => { + const invalidData = { data: null }; + expect(() => schema.parse(invalidData)).toThrow(); + }); + + it('should throw an error when data is missing', () => { + const invalidData = {}; + expect(() => schema.parse(invalidData)).toThrow(); + }); + + it('should throw an error when additional properties are included', () => { + const invalidData = { + data: { id: '1', type: 'example' }, + extra: 'invalid', + }; + expect(() => schema.parse(invalidData)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts new file mode 100644 index 00000000..d0c245b9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +import { zodData } from '../zod-share'; + +export const zodPostRelationship = z + .object({ + data: z.union([zodData(), zodData().array().nonempty()]), + }) + .strict(); + +export type ZodPostRelationship = typeof zodPostRelationship; +export type PostRelationship = z.infer; +export type PostRelationshipData = PostRelationship['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts new file mode 100644 index 00000000..61bb555b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts @@ -0,0 +1,219 @@ +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeField, + TypeForId, +} from '../../types'; +import { Users } from '../../../../mock-utils'; +import { zodPost, PostData } from './'; +import { ZodError } from 'zod'; + +const typeId: TypeForId = TypeField.number; +const typeName = 'Users'; +const fieldWithType: FieldWithType = { + id: TypeField.number, + login: TypeField.string, + firstName: TypeField.string, + testReal: TypeField.array, + testArrayNull: TypeField.array, + lastName: TypeField.string, + isActive: TypeField.boolean, + createdAt: TypeField.date, + testDate: TypeField.date, + updatedAt: TypeField.date, +}; +const propsDb: PropsForField = { + id: { type: 'number', isArray: false, isNullable: false }, + login: { type: 'string', isArray: false, isNullable: false }, + firstName: { type: 'string', isArray: false, isNullable: true }, + testReal: { type: 'number', isArray: true, isNullable: false }, + testArrayNull: { type: 'number', isArray: true, isNullable: true }, + lastName: { type: 'string', isArray: false, isNullable: true }, + isActive: { type: 'boolean', isArray: false, isNullable: true }, + createdAt: { type: 'date', isArray: false, isNullable: true }, + testDate: { type: 'date', isArray: false, isNullable: true }, + updatedAt: { type: 'date', isArray: false, isNullable: true }, +}; +const primaryColumn: EntityProps = 'id'; +const relationArrayProps: RelationPropsArray = { + roles: true, + comments: true, + notes: true, + addresses: false, + userGroup: false, + manager: false, +}; +const relationPopsName: RelationPropsTypeName = { + roles: 'Roles', + comments: 'Comments', + notes: 'Notes', + addresses: 'Addresses', + userGroup: 'UserGroups', + manager: 'Users', +}; +const primaryColumnType: RelationPrimaryColumnType = { + roles: TypeField.number, + userGroup: TypeField.number, + manager: TypeField.number, + addresses: TypeField.number, + comments: TypeField.number, + notes: TypeField.string, +}; +const schema = zodPost( + typeId, + 'users', + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType +); + +describe('zodPost', () => { + it('should be ok', () => { + const real = 123.123; + const date = new Date(); + const attributes = { + login: 'login', + lastName: 'sdfsdf', + isActive: true, + testDate: date.toISOString(), + testReal: [`${real}`], + testArrayNull: null, + }; + const relationships = { + notes: { + data: [ + { + type: 'notes', + id: 'dsfsdf', + }, + ], + }, + }; + const check = { + data: { + type: 'users', + attributes, + relationships, + }, + }; + const check2 = { + data: { + type: 'users', + attributes, + }, + }; + const check3 = { + data: { + id: '1', + type: 'users', + attributes, + }, + }; + + const checkResult = { + data: { + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + relationships, + }, + }; + const checkResult2 = { + data: { + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + }, + }; + const checkResult3 = { + data: { + id: '1', + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + }, + }; + + expect(schema.parse(check)).toEqual(checkResult); + expect(schema.parse(check2)).toEqual(checkResult2); + expect(schema.parse(check3)).toEqual(checkResult3); + }); + it('should be not ok', () => { + const check1 = {}; + const check2 = null; + const check3: unknown[] = []; + const check4 = ''; + const check5 = { + sdf: 'sdf', + }; + const check6 = { + data: {}, + }; + const check7 = { + data: { + type: 'users', + }, + }; + const check8 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + isActive: true, + }, + relationships: { + notes: [ + { + type: 'sdfsdf', + id: 'dsfsdf', + }, + ], + }, + }, + }; + const check9 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + id: 1, + }, + }, + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts new file mode 100644 index 00000000..74302679 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts @@ -0,0 +1,109 @@ +import { z, ZodObject, ZodOptional } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { + ZodId, + zodId, + ZodType, + zodType, + ZodAttributes, + zodAttributes, + ZodRelationships, + zodRelationships, +} from '../zod-share'; +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeForId, +} from '../../types'; + +type ZodInputPostShape = { + id: ZodOptional; + type: ZodType; + attributes: ZodAttributes; + relationships: ZodOptional>; +}; + +type ZodInputPostSchema = ZodObject< + ZodInputPostShape, + 'strict' +>; + +type ZodInputPostDataShape = { + data: ZodInputPostSchema; +}; + +function getShape( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodInputPostSchema { + const shape = { + id: zodId(typeId).optional(), + type: zodType(typeName), + attributes: zodAttributes(fieldWithType, propsDb, primaryColumn, false), + relationships: zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + false + ).optional(), + }; + + return z.object(shape).strict(); +} + +function zodDataShape( + shape: ZodInputPostSchema +): ZodPost { + return z + .object({ + data: shape, + }) + .strict(); +} + +export function zodPost( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodPost { + const shape = getShape( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + + return zodDataShape(shape); +} + +export type ZodPost = ZodObject< + ZodInputPostDataShape, + 'strict' +>; +export type Post = z.infer< + ZodPost +>; +export type PostData = Post< + E, + N +>['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts new file mode 100644 index 00000000..3486cbae --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts @@ -0,0 +1,111 @@ +import { zodFieldsInputQuery } from './fields'; +import { ResultGetField } from '../../types'; +import { Users } from '../../../../mock-utils'; + +describe('zodFieldsInputQuerySchema', () => { + const validRelationList: ResultGetField['relations'] = [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ]; + const schema = zodFieldsInputQuery(validRelationList); + + it('should validate successfully with a valid target and relation', () => { + const targetInput = 'field1,field2'; + const commentsInput = 'text,createdAt'; + const input = { + target: targetInput, + roles: '', + comments: commentsInput, + }; + const result = schema.safeParse(input); + expect.assertions(4); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveProperty('target', targetInput.split(',')); + expect(result.data).toHaveProperty('comments', commentsInput.split(',')); + expect(result.data).not.toHaveProperty('roles'); + } + }); + + it('should be null result', () => { + const input = { + target: '', + }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toBe(null); + } + }); + + it('should throw error if target is missing', () => { + const input = { + posts: ['content'], + }; + + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Validation error: Fields should be have only props' + ); + } + }); + + it('Not allow null', () => { + const input = { + target: 'inputString', + comments: null, + }; + + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Expected string, received null' + ); + } + }); + + it('should return null if all fields in input are empty or null', () => { + const input = { + target: undefined, + }; + + expect(schema.parse(input)).toBeNull(); + }); + + it('should throw error if additional fields are present in the input', () => { + const input = { + target: null, + notes: false, + comments: 1, + addresses: ['invalidValue'], // Invalid field + }; + + const result = schema.safeParse(input); + expect.assertions(5); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Expected string, received null' + ); + expect(result.error.issues[1].message).toContain( + 'Expected string, received boolean' + ); + expect(result.error.issues[2].message).toContain( + 'Expected string, received number' + ); + expect(result.error.issues[3].message).toContain( + 'Expected string, received array' + ); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts new file mode 100644 index 00000000..9bd95c11 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts @@ -0,0 +1,59 @@ +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { ResultGetField, ZodInfer } from '../../types'; +import { nonEmptyObject, getValidationErrorForStrict } from '../zod-utils'; + +function getZodRules() { + return z + .string() + .optional() + .transform((input) => (input ? input.split(',') : undefined)); +} + +type ZodRule = ReturnType; + +export function zodFieldsInputQuery( + relationList: ResultGetField['relations'] +) { + const target = z.object({ + target: getZodRules(), + }); + + const relation = relationList.reduce( + (acum, item) => ({ + ...acum, + [item]: getZodRules(), + }), + {} as { + [K in ResultGetField['relations'][number]]: ZodRule; + } + ); + + return target + .merge(z.object(relation)) + .strict(getValidationErrorForStrict(['target', ...relationList], 'Fields')) + .refine(nonEmptyObject(), { + message: 'Validation error: Need select field for target or relation', + }) + .optional() + .transform((input) => { + if (!input) return null; + + const result = ObjectTyped.entries(input).reduce((acum, [key, value]) => { + if (!value || value.length === 0) return acum; + + return { + ...acum, + [key]: value, + }; + }, {} as typeof input); + + return Object.keys(result).length > 0 ? result : null; + }); +} + +export type ZodFieldsInputQuery = ZodInfer< + typeof zodFieldsInputQuery +>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts new file mode 100644 index 00000000..919c41df --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts @@ -0,0 +1,123 @@ +import { zodFilterInputQuery } from './filter'; +import { Users } from '../../../../mock-utils'; +import { RelationTree, ResultGetField } from '../../types'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +describe('zodFilterInputQuery', () => { + it('should return transformed result with relation and target when valid data is provided', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + const input = { + login: { eq: 'johndoe' }, + addresses: { eq: 'null' }, + manager: { eq: null }, + userGroup: { ne: null }, + 'addresses.city': { eq: 'New York' }, + }; + + const result = schema.parse(input); + + expect(result).toEqual({ + relation: { + addresses: { city: { eq: 'New York' } }, + }, + target: { + addresses: { eq: null }, + manager: { eq: null }, + login: { eq: 'johndoe' }, + userGroup: { ne: null }, + }, + }); + }); + + it('should return null relation and target when no data is provided', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + + const result = schema.parse({}); + expect(result).toEqual({ relation: null, target: null }); + }); + + it('should ignore invalid fields and not include them in the result', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + const input = { + invalidField: { eq: 'should be ignored' }, + login: { eq: 'johndoe' }, + }; + + const result = schema.parse(input); + + expect(result).toEqual({ + relation: null, + target: { login: { eq: 'johndoe' } }, + }); + }); + + it('should handle nested relations correctly', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + const input = { + 'manager.firstName': { like: 'Jane' }, + 'manager.lastName': { nin: 'Doe,Jim' }, + }; + + const result = schema.parse(input); + + expect(result).toEqual({ + relation: { + manager: { + firstName: { like: 'Jane' }, + lastName: { nin: ['Doe', 'Jim'] }, + }, + }, + target: null, + }); + }); + + it('should throw a validation error for invalid structures', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + + const invalidInput = { + login: { unknownOperator: 'invalid' }, + }; + + expect(() => schema.parse(invalidInput)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts new file mode 100644 index 00000000..9586fcbb --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts @@ -0,0 +1,196 @@ +import { + FilterOperand, + ObjectTyped, + isString, +} from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; + +import { + RelationTree, + ResultGetField, + UnionToTuple, + ZodInfer, +} from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +import { oneOf, stringLongerThan } from '../zod-utils'; + +const arrayOp = { + [FilterOperand.in]: true, + [FilterOperand.nin]: true, + [FilterOperand.some]: true, +}; +function convertToFilterObject( + value: Record | string +): Partial<{ + [key in FilterOperand]: string | string[]; +}> { + if (isString | string, string>(value)) { + return { + [FilterOperand.eq]: value, + }; + } else { + return Object.entries(value).reduce((acum, [op, filed]) => { + if (op in arrayOp) { + acum[op] = (isString(filed) ? filed.split(',') : []).filter((i) => !!i); + } else { + acum[op] = filed; + } + return acum; + }, {} as Record); + } +} + +type FilterType = Partial<{ + [key in FilterOperand]: string | string[]; +}>; + +type OutPutFilter = { + relation: null | Record>; + target: null | Record; +}; + +function getZodRulesForRelation() { + return z + .union([ + z + .object({ + [FilterOperand.eq]: z + .union([z.literal('null'), z.null()]) + .transform(() => null), + }) + .strict(), + z + .object({ + [FilterOperand.ne]: z + .union([z.literal('null'), z.null()]) + .transform(() => null), + }) + .strict(), + ]) + .optional(); +} + +function getZodRulesForFilterOperator() { + const filterConditional = z + .union([z.string().refine(stringLongerThan()), z.null(), z.number()]) + .transform((r) => `${r}`); + + const conditional = z + .object( + ObjectTyped.values(FilterOperand).reduce((acum, item) => { + acum[item] = filterConditional; + return acum; + }, {} as Record) + ) + .strict() + .partial() + .refine(oneOf(Object.values(FilterOperand)), { + message: `Must have one of: "${Object.values(FilterOperand).join( + '","' + )}"`, + }); + + return z.union([filterConditional, conditional]).optional(); +} + +function shapeForArray< + R extends readonly [string, ...string[]], + Z extends ZodRulesForRelation | ZodRulesForFilterOperator +>(fields: R, zodSchema: Z) { + return fields.reduce( + (acum, item) => ({ + ...acum, + [item]: zodSchema, + }), + {} as { + [K in R[number]]: Z; + } + ); +} + +function getTupleConcatRelationFields( + relationList: RelationTree +): UnionToTuple< + { + [K in keyof RelationTree]: `${K & string}.${RelationTree[K][number]}`; + }[keyof RelationTree] +> { + const result: string[] = []; + + for (const [key, val] of ObjectTyped.entries(relationList)) { + const relName = key.toString(); + for (const v of val) { + result.push(`${relName}.${v}`); + } + } + + return result as any; +} + +type ZodRulesForRelation = ReturnType; +type ZodRulesForFilterOperator = ReturnType< + typeof getZodRulesForFilterOperator +>; + +export function zodFilterInputQuery( + fields: ResultGetField['field'], + relationList: RelationTree +) { + const target = z.object( + shapeForArray(fields, getZodRulesForFilterOperator()) + ); + + const relationTuple = ObjectTyped.keys(relationList) as UnionToTuple< + keyof RelationTree + >; + + const relations = z.object( + shapeForArray(relationTuple, getZodRulesForRelation()) + ); + + const relationFields = z.object( + shapeForArray( + getTupleConcatRelationFields(relationList), + getZodRulesForFilterOperator() + ) + ); + + return target + .merge(relations) + .merge(relationFields) + .optional() + .transform((data) => { + if (!data) { + return { + relation: null, + target: null, + }; + } + return Object.entries(data).reduce( + (acum, [field, value]: [string, any]) => { + const objectOperand = convertToFilterObject(value); + if (Object.keys(objectOperand).length === 0) { + return acum; + } + + if (field.indexOf('.') > -1) { + const [relation, fieldRelation] = field.split('.'); + acum['relation'] = !acum['relation'] ? {} : acum['relation']; + acum['relation'][relation] = acum['relation'][relation] || {}; + acum['relation'][relation][fieldRelation] = objectOperand; + } else { + acum['target'] = !acum['target'] ? {} : acum['target']; + acum['target'][field] = objectOperand; + } + + return acum; + }, + { relation: null, target: null } as OutPutFilter + ); + }); +} + +export type ZodFilterInputQuery = ZodInfer< + typeof zodFilterInputQuery +>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts new file mode 100644 index 00000000..3a202259 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts @@ -0,0 +1,32 @@ +import { zodIncludeInputQuery } from './include'; + +describe('zodIncludeInputQuerySchema', () => { + const schema = zodIncludeInputQuery(); + + it('should return null when input is undefined', () => { + const result = schema.parse(undefined); + expect(result).toBeNull(); + }); + + it('should return null when input is number, null, boolean, array', () => { + expect(() => schema.parse(123 as any)).toThrow(); + expect(() => schema.parse(null as any)).toThrow(); + expect(() => schema.parse(false as any)).toThrow(); + expect(() => schema.parse([] as any)).toThrow(); + }); + + it('should split a comma-separated string into an array of trimmed values', () => { + const result = schema.parse('a, b ,c , d'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it('should filter out empty values from the resulting array', () => { + const result = schema.parse('a, , b, , c'); + expect(result).toEqual(['a', 'b', 'c']); + }); + + it('should return null for an empty string', () => { + const result = schema.parse(''); + expect(result).toBeNull(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts new file mode 100644 index 00000000..639d34c8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { isString } from '@klerick/json-api-nestjs-shared'; +import { ZodInfer } from '../../types'; + +export function zodIncludeInputQuery() { + return z + .string() + .optional() + .transform((data) => { + if (!data || !isString(data)) return null; + return data + .split(',') + .map((i) => i.trim()) + .filter((i) => !!i); + }); +} + +export type ZodIncludeInputQuery = ZodInfer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts new file mode 100644 index 00000000..7873d08c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts @@ -0,0 +1,136 @@ +import { QueryField, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { zodInputQuery } from './index'; + +import { + RelationTree, + ResultGetField, + TupleOfEntityRelation, +} from '../../types'; +import { Users } from '../../../../mock-utils'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +const entityFieldsStructure = { + field: userFields, + relations: ObjectTyped.keys(userRelations) as TupleOfEntityRelation, +}; + +describe('zodInputQuery', () => { + it('should validate a correct input query object', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: { + target: 'login', + roles: 'name', + manager: 'firstName', + }, + [QueryField.filter]: { id: 1 }, + [QueryField.include]: 'manager,roles', + [QueryField.sort]: 'login', + [QueryField.page]: { number: 1, size: 10 }, + }; + try { + schema.parse(input); + } catch (e) { + console.log(e); + } + + expect(() => schema.parse(input)).not.toThrow(); + }); + + it('should throw an error for an invalid field in the input query', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: ['invalidRelation.invalidField'], + [QueryField.filter]: { id: 1 }, + [QueryField.include]: ['addresses'], + [QueryField.sort]: [{ field: 'login', order: 'asc' }], + [QueryField.page]: { number: 2, size: 5 }, + }; + + expect(() => schema.parse(input)).toThrow(); + }); + + it('should throw an error if an unexpected key is present in the input query', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: ['roles.name'], + [QueryField.filter]: { id: 1 }, + [QueryField.include]: ['roles'], + [QueryField.sort]: [{ field: 'login', order: 'asc' }], + [QueryField.page]: { number: 1, size: 10 }, + unexpectedKey: 'unexpectedValue', + }; + + expect(() => schema.parse(input)).toThrow(); + }); + + it('should throw an error when a required field is missing', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: ['roles.name'], + [QueryField.include]: ['roles'], + [QueryField.sort]: [{ field: 'login', order: 'asc' }], + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schema.parse(input)).toThrow(); + }); + + it('should validate input with empty but valid fields', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schema.parse(input)).not.toThrow(); + const result = schema.parse(input); + + expect(result).toEqual({ + fields: null, + filter: { relation: null, target: null }, + include: null, + sort: null, + page: { size: 10, number: 1 }, + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts new file mode 100644 index 00000000..04e80722 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts @@ -0,0 +1,39 @@ +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; +import { RelationTree, ResultGetField } from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +import { zodFieldsInputQuery } from './fields'; +import { zodFilterInputQuery } from './filter'; +import { zodIncludeInputQuery } from './include'; +import { zodSortInputQuery } from './sort'; +import { zodPageInputQuery } from '../zod-share'; + +export function zodInputQuery( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree +) { + return z + .object({ + [QueryField.fields]: zodFieldsInputQuery( + entityFieldsStructure.relations + ), + [QueryField.filter]: zodFilterInputQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.include]: zodIncludeInputQuery(), + [QueryField.sort]: zodSortInputQuery(), + [QueryField.page]: zodPageInputQuery(), + }) + .strict( + `Query object should contain only allow params: "${Object.keys( + QueryField + ).join('"."')}"` + ); +} + +export type ZodInputQuery = ReturnType< + typeof zodInputQuery +>; +export type InputQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts new file mode 100644 index 00000000..682fa534 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts @@ -0,0 +1,56 @@ +import { zodSortInputQuery } from './sort'; +import { ASC, DESC } from '../../../../constants'; + +describe('zodSortInputQuerySchema', () => { + const schema = zodSortInputQuery(); + + it('should transform a single field sort to the correct format', () => { + const result = schema.parse('name'); + expect(result).toEqual({ target: { name: ASC } }); + }); + + it('should transform a descending field sort to the correct format', () => { + const result = schema.parse('-name'); + expect(result).toEqual({ target: { name: DESC } }); + }); + + it('should transform multiple fields sort to the correct format', () => { + const result = schema.parse('name,-age'); + expect(result).toEqual({ target: { name: ASC, age: DESC } }); + }); + + it('should handle nested fields sort properly', () => { + const result = schema.parse('user.name,-user.age'); + expect(result).toEqual({ + user: { name: ASC, age: DESC }, + }); + }); + + it('should ignore empty or invalid inputs between commas', () => { + const result = schema.parse('name,,,-age'); + expect(result).toEqual({ target: { name: ASC, age: DESC } }); + }); + + it('should return null for empty input', () => { + const result = schema.parse(''); + expect(result).toBeNull(); + }); + + it('should trim spaces and process input correctly', () => { + const result = schema.parse(' name , -age '); + expect(result).toEqual({ target: { name: ASC, age: DESC } }); + }); + + it('should handle a mix of nested and non-nested fields', () => { + const result = schema.parse('user.name,-age,user.email'); + expect(result).toEqual({ + user: { name: ASC, email: ASC }, + target: { age: DESC }, + }); + }); + + it('should return null if the input is undefined', () => { + const result = schema.parse(undefined); + expect(result).toBeNull(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts new file mode 100644 index 00000000..105fab4e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { ASC, DESC } from '../../../../constants'; +import { ZodInfer } from '../../types'; + +export function zodSortInputQuery() { + return z + .string() + .optional() + .transform((data) => { + if (!data) return null; + + return data + .split(',') + .map((i) => i.trim()) + .filter((i) => !!i) + .reduce((acum, field) => { + const fieldName = + field.charAt(0) === '-' ? field.substring(1) : field; + const sort = field.charAt(0) === '-' ? DESC : ASC; + if (fieldName.indexOf('.') > -1) { + const [relation, fieldRelation] = field.split('.'); + const relationName = + relation.charAt(0) === '-' ? relation.substring(1) : relation; + + acum[relationName] = acum[relationName] || {}; + acum[relationName][fieldRelation] = sort; + } else { + acum['target'] = acum['target'] || {}; + acum['target'][fieldName] = sort; + } + + return acum; + }, {} as Record>); + }); +} + +export type ZodSortInputQuery = ZodInfer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts new file mode 100644 index 00000000..1c0d88f1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts @@ -0,0 +1,160 @@ +import { zodFieldsQuery } from './fields'; +import { Users } from '../../../../mock-utils'; +import { RelationTree, ResultGetField } from '../../types'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; +const schema = zodFieldsQuery(userFields, userRelations); +describe('zodFieldsQuerySchema', () => { + it('should validate a target field correctly', () => { + const input = { target: ['id'] }; + const result = schema.safeParse(input); + + expect(result.success).toBe(true); + }); + + it('should validate a nested relation field correctly', () => { + const input = { roles: ['isDefault', 'key'] }; + const result = schema.safeParse(input); + + expect(result.success).toBe(true); + }); + + it('should fail validation if input is empty', () => { + const input = {}; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe( + 'Validation error: Select target or relation fields' + ); + } + }); + + it('should fail validation if an invalid target field is provided', () => { + const input = { target: 'invalid_field' }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain('Expected array'); + } + }); + + it('should fail validation if the relation object contains incorrect fields', () => { + const input = { posts: { invalidField: 'value' } }; + const result = schema.safeParse(input); + + expect(result.success).toBe(false); + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = { target: ['id'], extraField: 'not_allowed' }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Should be only target of relation' + ); + } + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = { target: ['id', 'id'] }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Field should be unique' + ); + } + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = { target1: ['id', 'id'] }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Should be only target of relation' + ); + } + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = {}; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Validation error: Select target or relation fields' + ); + } + }); + it('should ensure the schema is strict and does not allow extra fields', () => { + const input: [] = []; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain('Expected object'); + } + }); + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = null; + const result = schema.safeParse(input); + expect(result.success).toBe(true); + }); + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = ''; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain('Expected object'); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts new file mode 100644 index 00000000..b301c636 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts @@ -0,0 +1,53 @@ +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; + +import { nonEmptyObject, uniqueArray } from '../zod-utils'; +import { ObjectLiteral } from '../../../../types'; +import { ResultGetField, RelationTree } from '../../types'; + +function getZodRules(fields: K) { + return z + .enum(fields) + .array() + .nonempty() + .refine(uniqueArray(), { + message: 'Field should be unique', + }) + .optional(); +} + +type ZodRule = ReturnType< + typeof getZodRules +>; + +export function zodFieldsQuery( + fields: ResultGetField['field'], + relationList: RelationTree +) { + const target = { + target: getZodRules(fields), + }; + + const relation = {} as { + [K in keyof RelationTree]: ZodRule[K]>; + }; + + for (const [key, value] of ObjectTyped.entries(relationList)) { + relation[key] = getZodRules(value); + } + + return z + .object({ + ...target, + ...relation, + }) + .strict('Should be only target of relation') + .refine(nonEmptyObject(), { + message: 'Validation error: Select target or relation fields', + }) + .nullable(); +} +export type ZodFieldsQuery = ReturnType< + typeof zodFieldsQuery +>; +export type FieldsQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts new file mode 100644 index 00000000..43972032 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts @@ -0,0 +1,381 @@ +import { zodFilterQuery } from './filter'; +import { Users } from '../../../../mock-utils'; +import { + RelationTree, + ResultGetField, + AllFieldWithType, + TypeField, + ArrayPropsForEntity, +} from '../../types'; +import { ZodError } from 'zod'; +import { ZodFilterInputQuery } from '../zod-input-query-schema/filter'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +const propsArray: ArrayPropsForEntity = { + target: { + testArrayNull: true, + testReal: true, + }, + addresses: { + arrayField: true, + }, + userGroup: {}, + manager: { + testArrayNull: true, + testReal: true, + }, + comments: {}, + notes: {}, + roles: {}, +}; + +const propsType: AllFieldWithType = { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + addresses: { + arrayField: TypeField.array, + country: TypeField.string, + state: TypeField.string, + city: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + manager: { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + }, + roles: { + isDefault: TypeField.boolean, + key: TypeField.string, + name: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + comments: { + kind: TypeField.string, + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + notes: { + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.string, + }, + userGroup: { + label: TypeField.string, + id: TypeField.number, + }, +}; + +const schema = zodFilterQuery( + userFields, + userRelations, + propsArray, + propsType +); + +describe('Check "filter" zod schema', () => { + describe('Valid schema', () => { + it('Valid schema - check1', () => { + const check1: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + ne: '12', + }, + }, + relation: null, + }; + const result = schema.parse(check1); + expect(result).toEqual(check1); + }); + + it('Valid schema - check2', () => { + const check2: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + }, + login: { + lt: 'sdfs', + }, + }, + relation: { + addresses: { + arrayField: { + some: ['sdfsdf', 'sdfsdf'], + }, + }, + }, + }; + const result = schema.parse(check2); + expect(result).toEqual(check2); + }); + + it('Valid schema - check3', () => { + const check3: ZodFilterInputQuery = { + target: null, + relation: null, + }; + const result = schema.parse(check3); + expect(result).toEqual(check3); + }); + + it('Valid schema - check4', () => { + const check4: ZodFilterInputQuery = { + target: null, + relation: { + comments: { + id: { + lte: '123', + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + }, + }; + const result = schema.parse(check4); + expect(result).toEqual(check4); + }); + + it('Valid schema - check5', () => { + const check5: ZodFilterInputQuery = { + target: null, + relation: { + comments: { + id: { + in: ['1'], + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + }, + }; + const result = schema.parse(check5); + expect(result).toEqual(check5); + }); + + it('Valid schema - check6', () => { + const check6: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + ne: '123', + }, + addresses: { + eq: 'null', + }, + }, + relation: null, + }; + const result = schema.parse(check6); + expect(result).toEqual(check6); + }); + + it('Valid schema - check7', () => { + const check7: ZodFilterInputQuery = { + target: { + isActive: { + eq: 'true', + }, + }, + relation: null, + }; + const result = schema.parse(check7); + expect(result).toEqual(check7); + }); + + it('Valid schema - check8', () => { + const check8: ZodFilterInputQuery = { + target: { + createdAt: { + eq: '2023-12-08T09:40:58.020Z', + }, + }, + relation: null, + }; + const result = schema.parse(check8); + expect(result).toEqual(check8); + }); + + it('Valid schema - check9', () => { + const check9: ZodFilterInputQuery = { + target: { + createdAt: { + eq: 'null', + }, + }, + relation: null, + }; + const result = schema.parse(check9); + expect(result.target!.createdAt!.eq).toEqual(null); + result.target!.createdAt!.eq = 'null'; + expect(result).toEqual(check9); + }); + + it('Valid schema - check10', () => { + const check: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + ne: '123', + }, + addresses: { + eq: null as any, + }, + }, + relation: null, + }; + const result = schema.parse(check); + expect(result).toEqual({ + ...check, + target: { + ...check.target, + addresses: { + eq: 'null', + }, + }, + }); + }); + }); + + describe('Invalid schema', () => { + it('Invalid schema - check1', () => { + const check1 = null; + expect(() => schema.parse(check1)).toThrow(ZodError); + }); + + it('Invalid schema - check2', () => { + const check2 = {}; + expect(() => schema.parse(check2)).toThrow(ZodError); + }); + + it('Invalid schema - check3', () => { + const check3 = ''; + expect(() => schema.parse(check3)).toThrow(ZodError); + }); + + it('Invalid schema - check4', () => { + const check4 = 1; + expect(() => schema.parse(check4)).toThrow(ZodError); + }); + + it('Invalid schema - check5', () => { + const check5: any[] = []; + expect(() => schema.parse(check5)).toThrow(ZodError); + }); + + it('Invalid schema - check6', () => { + const check6 = { + target: null, + }; + expect(() => schema.parse(check6)).toThrow(ZodError); + }); + + it('Invalid schema - check7', () => { + const check7 = { + target: null, + relation: { + commentsasda: { + id: { + lte: 'sdfsdf', + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + }, + }; + expect(() => schema.parse(check7)).toThrow(ZodError); + }); + + it('Invalid schema - check8', () => { + const check8 = { + target: null, + relation: { + comment: { + id: { + lte: 'sdfsdf', + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + sdfsdf: {}, + }, + }; + expect(() => schema.parse(check8)).toThrow(ZodError); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts new file mode 100644 index 00000000..9653d77d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts @@ -0,0 +1,243 @@ +import { z, ZodOptional } from 'zod'; +import { + FilterOperand, + ObjectTyped, + FilterOperandOnlyInNin, + FilterOperandOnlySimple, +} from '@klerick/json-api-nestjs-shared'; + +import { + AllFieldWithType, + ArrayPropsForEntity, + RelationTree, + ResultGetField, + IsArray, + TypeField, + EntityProps, + TypeOfArray, + CastProps, + TypeCast, + PropsArray, +} from '../../types'; +import { ObjectLiteral } from '../../../../types'; +import { + stringLongerThan, + arrayItemStringLongerThan, + stringMustBe, + elementOfArrayMustBe, + oneOf, + guardIsKeyOfObject, + nonEmptyObject, +} from '../zod-utils'; + +const zodRuleForString = z.union([ + z.literal('null').transform(() => null), + z.string().refine(stringLongerThan(), { + message: 'String should be not empty', + }), +]); + +const zodRuleStringArray = zodRuleForString + .array() + .nonempty() + .refine(arrayItemStringLongerThan(0), { + message: 'Array should be not empty', + }); + +const zodNullRule = z.union([ + z.literal('null'), + z.literal(null).transform((r) => 'null' as const), +]); + +const zodRuleFilterRelationSchema = z.union([ + z + .object({ + [FilterOperand.eq]: zodNullRule, + }) + .strict(), + z + .object({ + [FilterOperand.ne]: zodNullRule, + }) + .strict(), +]); +const zodRuleForArrayField = z + .object({ [FilterOperand.some]: zodRuleStringArray }) + .strict(); + +function getZodRulesForField(type: TypeField = TypeField.string) { + const simpleShape = ObjectTyped.entries(FilterOperandOnlySimple).reduce( + (acum, [key, val]) => ({ + ...acum, + [val]: zodRuleForString + .refine(stringMustBe(type), { + message: `String should be as ${type}`, + }) + .optional(), + }), + {} as { + [K in FilterOperandOnlySimple]: ZodOptional; + } + ); + + const ninInShape = ObjectTyped.entries(FilterOperandOnlyInNin).reduce( + (acum, [key, val]) => ({ + ...acum, + [val]: zodRuleStringArray + .refine(elementOfArrayMustBe(type), { + message: `String should be as ${type}`, + }) + .optional(), + }), + {} as { + [K in FilterOperandOnlyInNin]: ZodOptional; + } + ); + return z + .object({ + ...simpleShape, + ...ninInShape, + }) + .strict() + .refine( + oneOf( + Object.values(FilterOperand).filter((i) => i !== FilterOperand.some) + ), + { + message: `Must have one of: "${Object.values(FilterOperand) + .filter((i) => i !== FilterOperand.some) + .join('","')}"`, + } + ); +} + +function getFilterPropsShapeForEntity( + fields: ResultGetField['field'], + propsArrayTarget: PropsArray, + propsType: AllFieldWithType +) { + return fields.reduce( + (acum, field) => ({ + ...acum, + [field]: (Reflect.get(propsArrayTarget, field) + ? zodRuleForArrayField + : getZodRulesForField(propsType[field as EntityProps]) + ).optional(), + }), + {} as FilterProps['field']> + ); +} + +function getZodRulesForRelationShape( + shape: FilterProps['field']> +) { + return z.object(shape).strict().optional().refine(nonEmptyObject()); +} + +type ZodRuleForString = typeof zodRuleForString; +type ZodRuleStringArray = typeof zodRuleStringArray; +type ZodRuleFilterRelationSchema = typeof zodRuleFilterRelationSchema; +type ZodRuleForArrayField = typeof zodRuleForArrayField; +type ZodRulesForField = ReturnType; +type ZodRulesForRelationShape = ReturnType< + typeof getZodRulesForRelationShape +>; + +type FilterProps< + E extends ObjectLiteral, + P extends readonly [string, ...string[]] +> = { + [Props in P[number]]: Props extends keyof E + ? IsArray extends true + ? ZodOptional + : ZodOptional + : never; +}; + +type RelationType = TypeCast< + TypeOfArray>, + ObjectLiteral +>; + +type RelationFilterProps = { + [R in keyof RelationTree]: ZodRulesForRelationShape>; +}; + +export function zodFilterQuery( + fields: ResultGetField['field'], + relationTree: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +) { + const { target: propsArrayTarget, ...otherRelationPropsArray } = propsArray; + + const fieldsFilterProps = getFilterPropsShapeForEntity( + fields, + propsArrayTarget, + propsType + ); + + const targetRelation = ObjectTyped.keys(relationTree).reduce( + (acum, item) => ({ + ...acum, + [item]: zodRuleFilterRelationSchema.optional(), + }), + {} as { + [K in ResultGetField['relations'][number]]: ZodOptional; + } + ); + + const relationFilterProps = ObjectTyped.keys(relationTree).reduce( + (acum, name) => { + type F = typeof name; + type RT = RelationType; + type RTF = ResultGetField['field']; + + guardIsKeyOfObject(otherRelationPropsArray, name); + const relationField = relationTree[name] as RTF; + const relationPropsArray = otherRelationPropsArray[ + name + ] as PropsArray; + const relationPropsType = propsType[name] as AllFieldWithType; + + const filterProps = getFilterPropsShapeForEntity( + relationField, + relationPropsArray, + relationPropsType + ); + + const zodFilter = getZodRulesForRelationShape(filterProps); + + return { + ...acum, + [name]: zodFilter.optional(), + }; + }, + {} as RelationFilterProps + ); + + const targetShapeFilter = { + target: z + .object({ + ...fieldsFilterProps, + ...targetRelation, + }) + .strict() + .optional() + .refine(nonEmptyObject()) + .nullable(), + relation: z + .object(relationFilterProps) + .strict() + .optional() + .refine(nonEmptyObject()) + .nullable(), + }; + + return z.object(targetShapeFilter).strict().refine(nonEmptyObject()); +} + +export type ZodFilterQuery = ReturnType< + typeof zodFilterQuery +>; +export type FilterQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts new file mode 100644 index 00000000..6283395c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts @@ -0,0 +1,53 @@ +import { zodIncludeQuery } from './include'; +import { ResultGetField } from '../../types'; +import { Users } from '../../../../mock-utils'; + +const relationList: ResultGetField['relations'] = [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', +]; +const schema = zodIncludeQuery(relationList); + +describe('zodIncludeQuery', () => { + it('should validate an array of relations successfully', () => { + const result = schema.parse(['comments', 'addresses']); + + expect(result).toEqual(['comments', 'addresses']); + }); + + it('should return null if input is null', () => { + const result = schema.parse(null); + + expect(result).toBeNull(); + }); + + it('should throw an error if the array is empty', () => { + expect(() => schema.parse([])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining( + 'Array must contain at least 1 element' + ), + }) + ); + }); + + it('should throw an error if the array has duplicate entries', () => { + expect(() => schema.parse(['addresses', 'addresses'])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('Include should have unique relation'), + }) + ); + }); + + it('should throw an error if the input contains invalid relations', () => { + expect(() => schema.parse(['invalid_relation'])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('Invalid enum value. Expected'), + }) + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts new file mode 100644 index 00000000..cf8e87a0 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; +import { ObjectLiteral } from '../../../../types'; +import { ResultGetField } from '../../types'; +import { uniqueArray } from '../zod-utils'; + +export function zodIncludeQuery( + relationList: ResultGetField['relations'] +) { + return z + .enum(relationList) + .array() + .nonempty() + .refine(uniqueArray(), { + message: 'Include should have unique relation', + }) + .nullable(); +} + +export type ZodIncludeQuery = ReturnType< + typeof zodIncludeQuery +>; +export type IncludeQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts new file mode 100644 index 00000000..71a5eb51 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts @@ -0,0 +1,238 @@ +import { FilterOperand, QueryField } from '@klerick/json-api-nestjs-shared'; +import { zodQuery } from './index'; +import { + AllFieldWithType, + ArrayPropsForEntity, + RelationTree, + ResultGetField, + TypeField, +} from '../../types'; +import { Users } from '../../../../mock-utils'; +import { InputQuery } from '../zod-input-query-schema'; +import { ASC } from '../../../../constants'; + +const userFields: ResultGetField = { + field: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + relations: [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ], +}; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +const propsArray: ArrayPropsForEntity = { + target: { + testArrayNull: true, + testReal: true, + }, + addresses: { + arrayField: true, + }, + userGroup: {}, + manager: { + testArrayNull: true, + testReal: true, + }, + comments: {}, + notes: {}, + roles: {}, +}; + +const propsType: AllFieldWithType = { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + addresses: { + arrayField: TypeField.array, + country: TypeField.string, + state: TypeField.string, + city: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + manager: { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + }, + roles: { + isDefault: TypeField.boolean, + key: TypeField.string, + name: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + comments: { + kind: TypeField.string, + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + notes: { + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.string, + }, + userGroup: { + label: TypeField.string, + id: TypeField.number, + }, +}; + +const schemaQuery = zodQuery( + userFields, + userRelations, + propsArray, + propsType +); + +describe('schemaQuery.parse', () => { + it('should successfully parse valid input', () => { + const validInput: InputQuery = { + [QueryField.fields]: { + target: [ + 'id', + 'login', + 'firstName', + 'lastName', + 'createdAt', + 'updatedAt', + ], + }, + [QueryField.filter]: { relation: null, target: null }, + [QueryField.include]: ['roles'], + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schemaQuery.parse(validInput)).not.toThrow(); + const result = schemaQuery.parse(validInput); + expect(result).toEqual(validInput); + }); + + it('should throw an error for invalid field values', () => { + const invalidInput = { + field: ['invalidField'], // Field not defined in userFields + relations: { + comments: ['text', 'id'], + }, + }; + + expect(() => schemaQuery.parse(invalidInput)).toThrow(); + }); + + it('should throw an error if a required relation field is missing', () => { + const invalidInput = { + field: ['updatedAt', 'createdAt', 'login'], + relations: {}, // Missing required relations + }; + + expect(() => schemaQuery.parse(invalidInput)).toThrowError(/invalid_type/i); + }); + + it('should handle nested relations', () => { + const validInput: InputQuery = { + [QueryField.fields]: { + target: [ + 'id', + 'login', + 'firstName', + 'lastName', + 'createdAt', + 'updatedAt', + ], + }, + [QueryField.filter]: { + relation: null, + target: { + id: { + [FilterOperand.in]: ['1', '2', '3'], + }, + }, + }, + [QueryField.include]: ['roles'], + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schemaQuery.parse(validInput)).not.toThrow(); + const result = schemaQuery.parse(validInput); + expect(result).toEqual(validInput); + }); + + it('should throw an error for invalid nested relations', () => { + const invalidInput = { + field: ['id', 'login'], + relations: { + manager: { + field: ['id', 'nonExistentField'], + relations: { + roles: ['id', 'name'], + }, + }, + }, + }; + + expect(() => schemaQuery.parse(invalidInput)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts new file mode 100644 index 00000000..4025636f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts @@ -0,0 +1,99 @@ +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { z, ZodObject } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { + AllFieldWithType, + ArrayPropsForEntity, + RelationTree, + ResultGetField, +} from '../../types'; + +import { zodFieldsQuery, ZodFieldsQuery } from './fields'; +import { zodFilterQuery, ZodFilterQuery } from './filter'; +import { zodSortQuery, ZodSortQuery } from './sort'; +import { zodIncludeQuery, ZodIncludeQuery } from './include'; +import { zodPageInputQuery, ZodPageInputQuery } from '../zod-share'; + +type Shape = { + [QueryField.fields]: ZodFieldsQuery; + [QueryField.filter]: ZodFilterQuery; + [QueryField.include]: ZodIncludeQuery; + [QueryField.sort]: ZodSortQuery; + [QueryField.page]: ZodPageInputQuery; +}; + +function getShape( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +): Shape { + return { + [QueryField.fields]: zodFieldsQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.filter]: zodFilterQuery( + entityFieldsStructure.field, + entityRelationStructure, + propsArray, + propsType + ), + [QueryField.include]: zodIncludeQuery(entityFieldsStructure.relations), + [QueryField.sort]: zodSortQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.page]: zodPageInputQuery(), + }; +} + +function getZodResultSchema( + shape: Shape +): ZodObject, 'strict'> { + return z.object(shape).strict(); +} + +export function zodQuery( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +): ZodResultSchema { + const shape = getShape( + entityFieldsStructure, + entityRelationStructure, + propsArray, + propsType + ); + return getZodResultSchema(shape); +} + +export type ZodResultSchema = ReturnType< + typeof getZodResultSchema +>; +export type ZodQuery = ReturnType>; +export type Query = z.infer>; + +function zodQueryOne( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +) { + return z + .object({ + [QueryField.fields]: zodFieldsQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.include]: zodIncludeQuery(entityFieldsStructure.relations), + }) + .strict(); +} + +export type ZodQueryOne = ReturnType< + typeof zodQueryOne +>; +export type QueryOne = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts new file mode 100644 index 00000000..519c931d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts @@ -0,0 +1,126 @@ +import { zodSortQuery } from './sort'; + +import { RelationTree, ResultGetField } from '../../types'; +import { Users } from '../../../../mock-utils'; +import { ASC, DESC } from '../../../../constants'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; +const schema = zodSortQuery(userFields, userRelations); +describe('zodSortQuery', () => { + it('should create a Zod schema with target and relations', () => { + const parsedData = schema.parse({ + target: { id: ASC }, + addresses: { country: DESC }, + manager: { lastName: ASC }, + roles: { name: DESC }, + comments: { kind: DESC }, + notes: { text: ASC }, + userGroup: { label: ASC }, + }); + + expect(parsedData).toEqual({ + target: { id: ASC }, + addresses: { country: DESC }, + manager: { lastName: ASC }, + roles: { name: DESC }, + comments: { kind: DESC }, + notes: { text: ASC }, + userGroup: { label: ASC }, + }); + }); + + it('should throw an error for an invalid field in target', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse({ + target: 'invalid_value', + }); + }).toThrowError(); + }); + + it('should throw an error for invalid fields in relations', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse({ + addresses: 'invalid_value', + }); + }).toThrowError(); + }); + + it('should allow partial relations and target', () => { + const schema = zodSortQuery(userFields, userRelations); + + const parsedData = schema.parse({ + target: { id: ASC }, + addresses: { country: DESC }, + }); + + expect(parsedData).toEqual({ + target: { id: ASC }, + addresses: { country: DESC }, + }); + }); + + it('should fail if an empty object is not allowed', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse({}); + }).toThrowError(); + }); + it('null should be valid', () => { + expect(schema.parse(null)).toBe(null); + }); + + it('should fail if the input is not a valid object', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse([]); + }).toThrowError(); + expect(() => { + schema.parse('invalid'); + }).toThrowError(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts new file mode 100644 index 00000000..f129fdb0 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts @@ -0,0 +1,60 @@ +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; + +import { RelationTree, ResultGetField } from '../../types'; +import { ObjectLiteral } from '../../../../types'; +import { SORT_TYPE } from '../../../../constants'; +import { nonEmptyObject } from '../zod-utils'; + +function getZodSortRule() { + return z.enum(SORT_TYPE).optional(); +} + +function getZodFieldRule(fields: F) { + const targetShape = fields.reduce( + (acum, item) => ({ + ...acum, + [item]: getZodSortRule(), + }), + {} as { [K in F[number]]: ZodSortRule } + ); + + return z.object(targetShape).strict().refine(nonEmptyObject()).optional(); +} + +type ZodSortRule = ReturnType; +type ZodFieldRule = ReturnType< + typeof getZodFieldRule +>; + +export function zodSortQuery( + fields: ResultGetField['field'], + relationList: RelationTree +) { + const zodRelationShape = {} as { + [K in keyof RelationTree]: ZodFieldRule[K]>; + }; + const zodTargetShape: { target: ZodFieldRule['field']> } = { + target: getZodFieldRule(fields), + }; + + for (const [key, val] of ObjectTyped.entries(relationList)) { + if (key === 'target') continue; + zodRelationShape[key] = getZodFieldRule(val); + } + + return z + .object({ + ...zodTargetShape, + ...zodRelationShape, + }) + .strict() + .partial() + .refine(nonEmptyObject()) + .nullable(); +} + +export type ZodSortQuery = ReturnType< + typeof zodSortQuery +>; +export type SortQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts new file mode 100644 index 00000000..b9b7d3a9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts @@ -0,0 +1,200 @@ +import { z, ZodError } from 'zod'; +import { zodAttributes, ZodAttributes, Attributes } from './attributes'; +import { Addresses, Users } from '../../../../mock-utils'; +import { FieldWithType, PropsForField, TypeField } from '../../types'; + +const fieldTypeUsers: FieldWithType = { + id: TypeField.number, + isActive: TypeField.boolean, + firstName: TypeField.string, + createdAt: TypeField.date, + lastName: TypeField.string, + login: TypeField.string, + testDate: TypeField.date, + updatedAt: TypeField.date, + testReal: TypeField.array, + testArrayNull: TypeField.array, +}; +const propsDb: PropsForField = { + id: { type: Number, isArray: false, isNullable: false }, + login: { type: 'varchar', isArray: false, isNullable: false }, + firstName: { type: 'varchar', isArray: false, isNullable: true }, + testReal: { type: 'real', isArray: true, isNullable: false }, + testArrayNull: { type: 'real', isArray: true, isNullable: true }, + lastName: { type: 'varchar', isArray: false, isNullable: true }, + isActive: { type: 'boolean', isArray: false, isNullable: true }, + createdAt: { type: 'timestamp', isArray: false, isNullable: true }, + testDate: { type: 'timestamp', isArray: false, isNullable: true }, + updatedAt: { type: 'timestamp', isArray: false, isNullable: true }, +}; +const fieldTypeAddresses: FieldWithType = { + id: TypeField.number, + arrayField: TypeField.array, + state: TypeField.string, + city: TypeField.string, + createdAt: TypeField.date, + updatedAt: TypeField.date, + country: TypeField.string, +}; + +describe('attributes', () => { + type SchemaTypeUsers = Attributes; + type SchemaTypeAddresses = Attributes; + + describe('Attributes for post', () => { + let schemaUsers: ZodAttributes; + let schemaAddresses: ZodAttributes; + beforeEach(() => { + schemaUsers = zodAttributes(fieldTypeUsers, propsDb, 'id', false); + schemaAddresses = zodAttributes( + fieldTypeAddresses, + {} as PropsForField, + 'id', + false + ); + }); + + it('should be ok', () => { + const date = new Date(); + const check: SchemaTypeUsers = { + login: 'login', + isActive: true, + lastName: 'sdsdf', + testReal: [123.123, 123.123], + testArrayNull: [], + testDate: date.toISOString() as any, + }; + + const check2: SchemaTypeAddresses = { + arrayField: ['test', 'test'], + state: 'state', + country: 'country', + city: 'city', + createdAt: date.toISOString() as any, + updatedAt: date.toISOString() as any, + }; + + const check3: SchemaTypeUsers = { + login: 'login', + isActive: true, + lastName: 'sdsdf', + testReal: [123.123, 123.123], + testArrayNull: null as any, + testDate: date.toISOString() as any, + }; + + expect(schemaUsers.parse(check)).toEqual({ + ...check, + testDate: date, + }); + expect(schemaAddresses.parse(check2)).toEqual({ + ...check2, + createdAt: date, + updatedAt: date, + }); + + expect(schemaUsers.parse(check3)).toEqual({ + ...check3, + testDate: date, + }); + }); + + it('should be not ok', () => { + const check = { + id: '1', + isActive: 'true', + lastName: 1, + }; + const check2 = { + arrayField: 'test', + }; + expect.assertions(2); + try { + expect(schemaUsers.parse(check)).toEqual(check); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + try { + schemaAddresses.parse(check2); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); + }); + + describe('Attributes for patch', () => { + let schemaUsers: ZodAttributes; + let schemaAddresses: ZodAttributes; + beforeEach(() => { + schemaUsers = zodAttributes( + fieldTypeUsers, + propsDb, + 'id', + true + ); + schemaAddresses = zodAttributes( + fieldTypeAddresses, + {} as PropsForField, + 'id', + true + ); + }); + + it('should be ok', () => { + const date = new Date(); + const check: SchemaTypeUsers = { + login: 'login', + isActive: true, + testDate: date.toISOString() as any, + }; + + const check2: SchemaTypeAddresses = { + arrayField: ['test', 'test'], + state: 'state', + createdAt: date.toISOString() as any, + updatedAt: date.toISOString() as any, + }; + + const check3: SchemaTypeUsers = { + testReal: [123.123, 123.123], + testArrayNull: null as any, + testDate: date.toISOString() as any, + }; + + expect(schemaUsers.parse(check)).toEqual({ + ...check, + testDate: date, + }); + expect(schemaAddresses.parse(check2)).toEqual({ + ...check2, + createdAt: date, + updatedAt: date, + }); + + expect(schemaUsers.parse(check3)).toEqual({ + ...check3, + testDate: date, + }); + }); + + it('should be not ok', () => { + const check = { + id: '1', + }; + const check2 = { + arrayField: 'test', + }; + expect.assertions(2); + try { + expect(schemaUsers.parse(check)).toEqual(check); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + try { + schemaAddresses.parse(check2); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts new file mode 100644 index 00000000..d00503f7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts @@ -0,0 +1,202 @@ +import { + EntityProps, + ObjectTyped, + TypeOfArray, +} from '@klerick/json-api-nestjs-shared'; +import { z, ZodArray, ZodNullable } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { + FieldWithType, + PropsFieldItem, + PropsForField, + TypeField, +} from '../../types'; +import { nonEmptyObject } from '../zod-utils'; + +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; + +function getZodRulesForNumber(isNullable: boolean) { + const schema = z.preprocess((x) => Number(x), z.number()); + if (isNullable) schema.nullable().optional(); + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForString(isNullable: boolean) { + const schema = z.string(); + if (isNullable) schema.nullable().optional(); + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForDate(isNullable: boolean) { + const schema = z.coerce.date(); + if (isNullable) schema.nullable().optional(); + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForBoolean(isNullable: boolean) { + const schema = z.boolean(); + if (isNullable) schema.nullable(); + return isNullable ? schema.optional() : schema; +} + +function getZodSchemaForJson(isNullable: boolean) { + const tmpSchema = isNullable ? literalSchema.nullable() : literalSchema; + + const schema: z.ZodType = z.lazy(() => + z.union([tmpSchema, z.array(tmpSchema), z.record(tmpSchema)]) + ); + + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForArray( + propsField: PropsFieldItem +): + | ZodArray, 'many'> + | ZodNullable, 'many'>> { + const type = propsField.type as T; + let schema: ZodRulesForArray; + + if (!propsField) { + schema = getZodRulesForString(false) as ZodRulesForArray; + } else { + switch (type) { + case 'number': + case 'real': + case 'integer': + case 'bigint': + case 'double': + case 'numeric': + case Number: + schema = getZodRulesForNumber(false) as ZodRulesForArray; + break; + case 'date': + case Date: + schema = getZodRulesForDate(false) as ZodRulesForArray; + break; + case 'boolean': + case Boolean: + schema = getZodRulesForBoolean(false) as ZodRulesForArray; + break; + default: + schema = getZodRulesForString(false) as ZodRulesForArray; + } + } + + if (propsField.isNullable) { + return schema.array().nullable() as ZodNullable< + ZodArray, 'many'> + >; + } + return schema.array() as ZodArray, 'many'>; +} + +type ZodRulesForArray = T extends number + ? ReturnType + : T extends Date + ? ReturnType + : T extends boolean + ? ReturnType + : ReturnType; + +type ZodRulesForType = ReturnType< + T extends TypeField.array + ? typeof getZodRulesForArray + : T extends TypeField.date + ? typeof getZodRulesForDate + : T extends TypeField.boolean + ? typeof getZodRulesForBoolean + : T extends TypeField.number + ? typeof getZodRulesForNumber + : T extends TypeField.object + ? typeof getZodSchemaForJson + : typeof getZodRulesForString +>; + +function buildSchema( + fieldType: T, + propsField: P +): ZodRulesForType { + let schema: ZodRulesForType; + switch (fieldType) { + case TypeField.array: + schema = getZodRulesForArray(propsField) as ZodRulesForType; + break; + case TypeField.date: + schema = getZodRulesForDate(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + case TypeField.boolean: + schema = getZodRulesForBoolean(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + case TypeField.number: + schema = getZodRulesForNumber(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + case TypeField.object: + schema = getZodSchemaForJson(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + default: + schema = getZodRulesForString(propsField.isNullable) as ZodRulesForType< + T, + I + >; + } + + return schema; +} + +export function zodAttributes< + E extends ObjectLiteral, + S extends true | false = false +>( + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + isPatch: S +) { + const objectShape = {} as { + [K in keyof Omit, keyof EntityProps>]: ZodRulesForType< + FieldWithType[K], + TypeOfArray + >; + }; + + for (const [nameList, type] of ObjectTyped.entries(fieldWithType)) { + if (nameList === primaryColumn) continue; + const name = nameList as keyof Omit, keyof EntityProps>; + const propsField = propsDb[name]; + objectShape[name] = buildSchema< + typeof type, + typeof propsField, + TypeOfArray + >(type, propsField || {}); + } + const zodSchema = z.object(objectShape).strict(); + if (isPatch) { + return zodSchema.partial().refine(nonEmptyObject()); + } + return zodSchema.refine(nonEmptyObject()); +} + +export type ZodAttributes< + E extends ObjectLiteral, + K extends true | false = false +> = ReturnType>; +export type Attributes< + E extends ObjectLiteral, + K extends true | false = false +> = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts new file mode 100644 index 00000000..d5ba401b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts @@ -0,0 +1,37 @@ +import { ZodError } from 'zod'; + +import { ZodId, zodId } from './id'; +import { TypeField } from '../../types'; + +describe('zodIdSchema', () => { + let numberStringSchema: ZodId; + let stringSchema: ZodId; + beforeAll(() => { + numberStringSchema = zodId(TypeField.number); + stringSchema = zodId(TypeField.string); + }); + + it('Should be correct', () => { + const check1 = '1'; + const check2 = '12'; + const check3 = '123'; + const check4 = '-123'; + + const check5 = 'sfdsf'; + const checkArray = [check1, check2, check3, check4]; + for (const item of checkArray) { + expect(numberStringSchema.parse(item)).toBe(item); + } + expect(stringSchema.parse(check5)).toBe(check5); + }); + + it('Should be not ok', () => { + expect.assertions(1); + + try { + numberStringSchema.parse('sdfdfsfsf'); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts new file mode 100644 index 00000000..030ed201 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; +import { TypeField, TypeForId } from '../../types'; + +const reg = new RegExp('^-?\\d+$'); + +export function zodId(typeId: TypeForId) { + let idSchema = z.string(); + if (typeId === TypeField.number) { + idSchema = idSchema.regex(reg); + } + + return idSchema; +} + +export type ZodId = ReturnType; +export type Id = z.infer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts new file mode 100644 index 00000000..4b1fc19a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts @@ -0,0 +1,6 @@ +export * from './page'; +export * from './id'; +export * from './type'; +export * from './attributes'; +export * from './rel-data'; +export * from './relationships'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts new file mode 100644 index 00000000..6123a0c1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts @@ -0,0 +1,42 @@ +import { zodPageInputQuery } from './page'; +import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../../constants'; + +describe('zodPageInputQuerySchema', () => { + const schema = zodPageInputQuery(); + + it('should parse valid size and number as number string', () => { + const result = schema.parse({ size: '5', number: '2' }); + expect(result).toEqual({ size: 5, number: 2 }); + }); + + it('should parse valid size and number', () => { + const result = schema.parse({ size: 5, number: 2 }); + expect(result).toEqual({ size: 5, number: 2 }); + }); + + it('should use the default size and number if not provided', () => { + const result = schema.parse({}); + expect(result).toEqual({ + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }); + }); + + it('should fail if size is less than 1', () => { + expect(() => schema.parse({ size: '0', number: '2' })).toThrow(); + }); + + it('should fail if number is less than 1', () => { + expect(() => schema.parse({ size: '5', number: '0' })).toThrow(); + }); + + it('should error size and number is number of float', () => { + expect(() => schema.parse({ size: '5.7', number: '2.9' })).toThrow(); + }); + + it('should error if has additional properties', () => { + expect(() => + schema.parse({ size: '5', number: '2', extra: 'ignored' }) + ).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts new file mode 100644 index 00000000..2682313b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; +import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../../constants'; +import { ObjectLiteral } from '../../../../types'; + +export function zodPageInputQuery() { + return z + .object({ + size: z + .preprocess((x) => Number(x), z.number().int().min(1)) + .default(DEFAULT_PAGE_SIZE), + number: z + .preprocess((x) => Number(x), z.number().int().min(1)) + .default(DEFAULT_QUERY_PAGE), + }) + .strict() + .default({ + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }); +} + +export type ZodPageInputQuery = ReturnType; +export type PageInputQuery = z.infer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts new file mode 100644 index 00000000..fe3e3122 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts @@ -0,0 +1,37 @@ +import { zodRelData, ZodRelData } from './rel-data'; +import { TypeField } from '../../types'; +import { ZodError } from 'zod'; + +describe('zodDataSchema', () => { + let zodData: ZodRelData; + beforeAll(() => { + zodData = zodRelData('users', TypeField.string); + }); + + it('Should be ok', () => { + const check = { + type: 'users', + id: 'id', + }; + expect(zodData.parse(check)).toEqual(check); + }); + + it('Should be not ok', () => { + const check = {}; + const check1 = { + test: '1', + }; + const check3: any[] = []; + const check4 = 'adfsdf'; + const check5 = true; + const checkArray = [check, check1, check3, check4, check5]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + zodData.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts new file mode 100644 index 00000000..619b9fc1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +import { TypeForId } from '../../types'; +import { nonEmptyObject } from '../zod-utils'; + +import { zodType, zodId } from './'; + +export function zodRelData(typeName: T, typeId: TypeForId) { + return z + .object({ + id: zodId(typeId), + type: zodType(typeName), + }) + .strict() + .refine(nonEmptyObject); +} + +export type ZodRelData = ReturnType>; +export type RelData = z.infer>; + +export function zodData() { + return z + .object({ + type: z.coerce.string(), + id: z.coerce.string(), + }) + .strict() + .refine(nonEmptyObject); +} + +export type ZodData = ReturnType; +export type Data = z.infer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts new file mode 100644 index 00000000..642a9397 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts @@ -0,0 +1,270 @@ +import { z, ZodError } from 'zod'; + +import { zodRelationships, ZodRelationships } from './relationships'; +import { Users } from '../../../../mock-utils'; +import { + RelationPropsArray, + RelationPropsTypeName, + RelationPrimaryColumnType, + TypeField, +} from '../../types'; + +describe('zodRelationships', () => { + const relationArrayProps: RelationPropsArray = { + roles: true, + userGroup: false, + notes: true, + addresses: false, + comments: true, + manager: false, + }; + const relationPopsName: RelationPropsTypeName = { + roles: 'Roles', + userGroup: 'UserGroups', + notes: 'Notes', + addresses: 'Addresses', + comments: 'Comments', + manager: 'Users', + }; + + const primaryColumnType: RelationPrimaryColumnType = { + roles: TypeField.number, + userGroup: TypeField.number, + notes: TypeField.string, + addresses: TypeField.number, + comments: TypeField.number, + manager: TypeField.number, + }; + + let relationshipsSchema: ZodRelationships; + + describe('POST', () => { + beforeAll(() => { + relationshipsSchema = zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + false + ); + }); + + it('Should be ok', () => { + const check = { + comments: { + data: [ + { + type: 'comments', + id: '1', + }, + ], + }, + userGroup: { + data: { + type: 'user-groups', + id: '1', + }, + }, + manager: { + data: { + type: 'users', + id: '1', + }, + }, + notes: { + data: [ + { + type: 'notes', + id: 'id', + }, + ], + }, + }; + expect(relationshipsSchema.parse(check)).toEqual(check); + }); + + it('should be not ok', () => { + const check1 = {}; + const check2 = ''; + const check3: any[] = []; + const check4 = true; + const check5 = { + sddsf: {}, + }; + const check6 = { + comments: [], + }; + const check7 = { + comments: {}, + }; + const check8 = { + comments: '', + }; + const check9 = { + comments: true, + }; + const check10 = { + comments: [ + { + sdsf: 'sdfsdf', + }, + ], + }; + const check11 = { + comments: [{}], + }; + const check12 = { + manager: {}, + }; + const check13 = { + manager: { + sdfs: 'sdsdf', + }, + }; + const check14 = { + manager: { + id: 'sdsdf', + type: 'users', + }, + }; + const check15 = { + manager: null, + }; + const check16 = { + manager: [], + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + check10, + check11, + check12, + check13, + check14, + check15, + check16, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + relationshipsSchema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + + describe('PATCH', () => { + beforeAll(() => { + relationshipsSchema = zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + true + ); + }); + + it('Should be ok', () => { + const check = { + comments: { + data: [], + }, + userGroup: { + data: null, + }, + manager: { + data: { + type: 'users', + id: '1', + }, + }, + notes: { + data: [ + { + type: 'notes', + id: 'id', + }, + ], + }, + }; + expect(relationshipsSchema.parse(check)).toEqual(check); + }); + + it('should be not ok', () => { + const check1 = {}; + const check2 = ''; + const check3: any[] = []; + const check4 = true; + const check5 = { + sddsf: {}, + }; + const check6 = { + comments: [], + }; + const check7 = { + comments: {}, + }; + const check8 = { + comments: '', + }; + const check9 = { + comments: true, + }; + const check10 = { + comments: [ + { + sdsf: 'sdfsdf', + }, + ], + }; + const check11 = { + comments: [{}], + }; + const check12 = { + manager: {}, + }; + const check13 = { + manager: { + sdfs: 'sdsdf', + }, + }; + const check14 = { + manager: { + id: 'sdsdf', + type: 'users', + }, + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + check10, + check11, + check12, + check13, + check14, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + relationshipsSchema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts new file mode 100644 index 00000000..425253b8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts @@ -0,0 +1,157 @@ +import { z } from 'zod'; +import { + camelToKebab, + KebabCase, + ObjectTyped, +} from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; + +import { + RelationPropsArray, + RelationPropsTypeName, + RelationPrimaryColumnType, + TypeForId, +} from '../../types'; +import { zodRelData } from './rel-data'; +import { nonEmptyObject } from '../zod-utils'; + +function getZodRuleForData< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + if (isPatch) { + return zodRelData(typeName, primaryType).nullable(); + } + return zodRelData(typeName, primaryType); +} + +function getZodRuleForArrayData< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + const dataArraySchema = getZodRuleForData( + typeName, + primaryType, + false + ).array(); + if (isPatch) { + return dataArraySchema; + } + return dataArraySchema.nonempty(); +} + +function getZodDataShape< + K extends string, + P extends TypeForId, + I extends true, + T extends true | false = false +>( + typeName: K, + primaryType: P, + isArray: I, + isPatch: T +): ReturnType>; +function getZodDataShape< + K extends string, + P extends TypeForId, + I extends false, + T extends true | false = false +>( + typeName: K, + primaryType: P, + isArray: I, + isPatch: T +): ReturnType>; +function getZodDataShape< + K extends string, + P extends TypeForId, + I extends boolean, + T extends true | false = false +>( + typeName: K, + primaryType: P, + isArray: I, + isPatch: T +): ReturnType< + typeof getZodRuleForArrayData | typeof getZodRuleForData +> { + return isArray + ? getZodRuleForArrayData(typeName, primaryType, isPatch) + : getZodRuleForData(typeName, primaryType, isPatch); +} + +function getZodResultData< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + return z + .object({ + data: getZodDataShape(typeName, primaryType, false, isPatch), + }) + .optional(); +} + +function getZodResultDataArray< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + return z + .object({ + data: getZodDataShape(typeName, primaryType, true, isPatch), + }) + .optional(); +} + +type ZodResultData< + K extends string, + P extends TypeForId, + I extends boolean, + T extends true | false = false +> = I extends true + ? ReturnType> + : ReturnType>; + +export function zodRelationships< + E extends ObjectLiteral, + S extends true | false = false +>( + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType, + isPatch: S +) { + const shape = {} as { + [K in keyof RelationPropsArray]: ZodResultData< + KebabCase[K]>, + RelationPrimaryColumnType[K], + RelationPropsArray[K], + S + >; + }; + + for (const [props, value] of ObjectTyped.entries(relationArrayProps)) { + const typeName = camelToKebab(relationPopsName[props]); + const primaryType = primaryColumnType[props]; + shape[props] = ( + value === true + ? getZodResultDataArray(typeName, primaryType, isPatch) + : getZodResultData(typeName, primaryType, isPatch) + ) as ZodResultData; + } + + return z.object(shape).strict().refine(nonEmptyObject()); +} + +export type ZodRelationships< + T extends ObjectLiteral, + K extends true | false = false +> = ReturnType>; +export type Relationships< + T extends ObjectLiteral, + K extends true | false = false +> = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts new file mode 100644 index 00000000..a5d5bd3b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts @@ -0,0 +1,21 @@ +import { zodType, ZodType } from './type'; +import { ZodError } from 'zod'; + +describe('type', () => { + const literal = 'users'; + let userTypeSchema: ZodType; + beforeAll(() => { + userTypeSchema = zodType(literal); + }); + it('should be ok', () => { + expect(userTypeSchema.parse(literal)).toEqual(literal); + }); + it('should be ok', () => { + expect.assertions(1); + try { + userTypeSchema.parse('test'); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts new file mode 100644 index 00000000..297952d1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export function zodType(type: T) { + return z.literal(type); +} + +export type ZodType = ReturnType>; +export type Type = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts new file mode 100644 index 00000000..decec6dc --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts @@ -0,0 +1,359 @@ +import { + arrayItemStringLongerThan, + elementOfArrayMustBe, + getValidationErrorForStrict, + nonEmptyObject, + oneOf, + stringLongerThan, + stringMustBe, + guardIsKeyOfObject, +} from './zod-utils'; +import { TypeField } from '../types'; + +describe('zod-utils', () => { + describe('guardIsKeyOfObject', () => { + /** + * Function Description: + * The `guardIsKeyOfObject` function acts as a type guard that ensures the given `key` + * is a valid key of the provided object `R`. If the key exists in the object, the type check passes. + * Otherwise, it throws an error. + */ + it('should not throw an error if the key exists in the object', () => { + const obj = { a: 1, b: 2, c: 3 }; + expect(() => guardIsKeyOfObject(obj, 'a')).not.toThrow(); + }); + + it('should throw an error if the key does not exist in the object', () => { + const obj = { a: 1, b: 2, c: 3 }; + expect(() => guardIsKeyOfObject(obj, 'z')).toThrow('Type guard error'); + }); + + it('should throw an error when the object is null', () => { + const obj = null; + expect(() => guardIsKeyOfObject(obj, 'a')).toThrow('Type guard error'); + }); + + it('should throw an error when the object is undefined', () => { + const obj = undefined; + expect(() => guardIsKeyOfObject(obj as any, 'a')).toThrow( + 'Type guard error' + ); + }); + + it('should throw an error for non-object types', () => { + const nonObject = 42; // A number instead of an object + expect(() => guardIsKeyOfObject(nonObject as any, 'a')).toThrow( + 'Type guard error' + ); + }); + + it('should work with symbol keys in the object', () => { + const symbolKey = Symbol('key'); + const obj = { [symbolKey]: 'value' }; + expect(() => guardIsKeyOfObject(obj, symbolKey)).not.toThrow(); + }); + + it('should throw an error if the symbol key does not exist', () => { + const existingSymbol = Symbol('existing'); + const missingSymbol = Symbol('missing'); + const obj = { [existingSymbol]: 'value' }; + expect(() => guardIsKeyOfObject(obj, missingSymbol)).toThrow( + 'Type guard error' + ); + }); + + it('should not throw an error if the number key exists in the object', () => { + const obj = { 1: 'one', 2: 'two' }; + expect(() => guardIsKeyOfObject(obj, 1)).not.toThrow(); + }); + + it('should throw an error if the number key does not exist in the object', () => { + const obj = { 1: 'one', 2: 'two' }; + expect(() => guardIsKeyOfObject(obj, 3)).toThrow('Type guard error'); + }); + + it('should not throw an error for empty object with no keys', () => { + const obj = {}; + expect(() => guardIsKeyOfObject(obj, 'a')).toThrow('Type guard error'); + }); + }); + describe('nonEmptyObject', () => { + it('should return true for a non-empty object', () => { + const isNonEmpty = nonEmptyObject()({ key: 'value' }); + expect(isNonEmpty).toBe(true); + }); + + it('should return false for an empty object', () => { + const isNonEmpty = nonEmptyObject()({}); + expect(isNonEmpty).toBe(false); + }); + + it('should return false for null value', () => { + const isNonEmpty = nonEmptyObject()(null); + expect(isNonEmpty).toBe(false); + }); + + it('should return false for undefined value', () => { + const isNonEmpty = nonEmptyObject()(undefined); + expect(isNonEmpty).toBe(false); + }); + + it('should return false for non-object values', () => { + const isNonEmptyString = nonEmptyObject()('string'); + expect(isNonEmptyString).toBe(false); + + const isNonEmptyNumber = nonEmptyObject()(42); + expect(isNonEmptyNumber).toBe(false); + const isNonEmptyArray = nonEmptyObject()([1, 2, 3]); + expect(isNonEmptyArray).toBe(true); + const isEmptyArray = nonEmptyObject()([]); + expect(isEmptyArray).toBe(false); + }); + }); + + describe('getValidationErrorForStrict', () => { + it('should return a validation error message for "Fields" with props', () => { + const message = getValidationErrorForStrict(['id', 'name'], 'Fields'); + expect(message).toBe( + 'Validation error: Fields should be have only props: ["id","name"]' + ); + }); + + it('should return a validation error message for "Filter" with props', () => { + const message = getValidationErrorForStrict(['age', 'gender'], 'Filter'); + expect(message).toBe( + 'Validation error: Filter should be have only props: ["age","gender"]' + ); + }); + + it('should return a message with an empty props array', () => { + const message = getValidationErrorForStrict([], 'Fields'); + expect(message).toBe( + 'Validation error: Fields should be have only props: [""]' + ); + }); + + it('should handle single prop correctly', () => { + const message = getValidationErrorForStrict(['id'], 'Fields'); + expect(message).toBe( + 'Validation error: Fields should be have only props: ["id"]' + ); + }); + + it('should handle a large props array', () => { + const props = ['a', 'b', 'c', 'd', 'e']; + const message = getValidationErrorForStrict(props, 'Filter'); + expect(message).toBe( + 'Validation error: Filter should be have only props: ["a","b","c","d","e"]' + ); + }); + }); + + describe('oneOf', () => { + it('should return true if at least one key exists in the object', () => { + const hasKey = oneOf(['key1', 'key2'])({ key1: 'value1' }); + expect(hasKey).toBe(true); + }); + + it('should return false if none of the keys exist in the object', () => { + const hasKey = oneOf(['missingKey1', 'missingKey2'])({ key1: 'value1' }); + expect(hasKey).toBe(false); + }); + + it('should return false for an empty object', () => { + const hasKey = oneOf(['key1', 'key2'])({}); + expect(hasKey).toBe(false); + }); + + it('should return false for null or undefined input', () => { + const hasKeyInNull = oneOf(['key1', 'key2'])(null); + expect(hasKeyInNull).toBe(false); + + const hasKeyInUndefined = oneOf(['key1', 'key2'])(undefined); + expect(hasKeyInUndefined).toBe(false); + }); + + it('should return false for an empty array of keys', () => { + const hasKey = oneOf([])({ key1: 'value1' }); + expect(hasKey).toBe(false); + }); + + it('should work with overlapping keys', () => { + const hasKey = oneOf(['key1', 'key2'])({ + key1: 'value1', + key2: 'value2', + }); + expect(hasKey).toBe(true); + }); + }); + + describe('stringLongerThan', () => { + it('should return true if the string is longer than the specified length', () => { + const isLongerThan = stringLongerThan(5)('testing'); + expect(isLongerThan).toBe(true); + }); + + it('should return false if the string length is equal to the specified length', () => { + const isLongerThan = stringLongerThan(7)('testing'); + expect(isLongerThan).toBe(false); + }); + + it('should return false if the string is shorter than the specified length', () => { + const isLongerThan = stringLongerThan(10)('test'); + expect(isLongerThan).toBe(false); + }); + + it('should return false for an empty string when length > 0', () => { + const isLongerThan = stringLongerThan(1)(''); + expect(isLongerThan).toBe(false); + }); + + it('should return true for length 0 with any non-empty string', () => { + const isLongerThan = stringLongerThan(0)('t'); + expect(isLongerThan).toBe(true); + }); + }); + + describe('arrayItemStringLongerThan', () => { + it('should return true if all strings in the array are longer than the specified length', () => { + const isAllLonger = arrayItemStringLongerThan(3)(['hello', 'world']); + expect(isAllLonger).toBe(true); + }); + + it('should return false if any string in the array is shorter than or equal to the specified length', () => { + const isAllLonger = arrayItemStringLongerThan(5)(['short', 'tiny']); + expect(isAllLonger).toBe(false); + }); + + it('should return false for an array with null values that fail the length check', () => { + const isAllLonger = arrayItemStringLongerThan(5)([null, 'tiny']); + expect(isAllLonger).toBe(false); + }); + + it('should return true for an array where all valid strings are longer than the specified length', () => { + const isAllLonger = arrayItemStringLongerThan(2)([null, 'hello', 'yes']); + expect(isAllLonger).toBe(true); + }); + + it('should return true if the array is empty', () => { + const isAllLonger = arrayItemStringLongerThan(3)([] as any); + expect(isAllLonger).toBe(true); + }); + + it('should return false if any string in the array is shorter and null does not interfere', () => { + const isAllLonger = arrayItemStringLongerThan(4)(['short', null, 'tiny']); + expect(isAllLonger).toBe(false); + }); + }); + + describe('stringMustBe', () => { + it('should return true for a null input', () => { + const isValid = stringMustBe()(null); + expect(isValid).toBe(true); + }); + + it('should return true for valid boolean strings', () => { + const isValidTrue = stringMustBe(TypeField.boolean)('true'); + const isValidFalse = stringMustBe(TypeField.boolean)('false'); + expect(isValidTrue).toBe(true); + expect(isValidFalse).toBe(true); + }); + + it('should return false for invalid boolean strings', () => { + const isValid = stringMustBe(TypeField.boolean)('yes'); + expect(isValid).toBe(false); + }); + + it('should return true for numeric strings', () => { + const isValid = stringMustBe(TypeField.number)('123'); + expect(isValid).toBe(true); + }); + + it('should return false for non-numeric strings when type is number', () => { + const isValid = stringMustBe(TypeField.number)('abc'); + expect(isValid).toBe(false); + }); + + it('should return true for valid ISO date strings', () => { + const isValid = stringMustBe(TypeField.date)('2023-10-10'); + expect(isValid).toBe(true); + }); + + it('should return false for invalid date strings', () => { + const isValid = stringMustBe(TypeField.date)('not-a-date'); + expect(isValid).toBe(false); + }); + + it('should return true for any input when type is string', () => { + const isValid = stringMustBe(TypeField.string)('any-value'); + expect(isValid).toBe(true); + }); + }); + + describe('elementOfArrayMustBe', () => { + it('should return true if all elements in the array are valid strings', () => { + const isValid = elementOfArrayMustBe(TypeField.string)([ + 'hello', + 'world', + ]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid string, because before converted to string', () => { + const isValid = elementOfArrayMustBe(TypeField.string)(['hello', 123]); + expect(isValid).toBe(true); + }); + + it('should return true if all elements in the array are valid numbers', () => { + const isValid = elementOfArrayMustBe(TypeField.number)([1, 2, 3]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid number', () => { + const isValid = elementOfArrayMustBe(TypeField.number)([1, 'two', 3]); + expect(isValid).toBe(false); + }); + + it('should return true if all elements are valid boolean strings', () => { + const isValid = elementOfArrayMustBe(TypeField.boolean)([ + 'true', + 'false', + ]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid boolean string', () => { + const isValid = elementOfArrayMustBe(TypeField.boolean)([ + 'true', + 'hello', + ]); + expect(isValid).toBe(false); + }); + + it('should return true for an empty array', () => { + const isValid = elementOfArrayMustBe(TypeField.string)([]); + expect(isValid).toBe(true); + }); + + it('should return true if array contains null', () => { + const isValid = elementOfArrayMustBe(TypeField.string)(['hello', null]); + expect(isValid).toBe(true); + }); + + it('should return true if all elements are valid dates', () => { + const isValid = elementOfArrayMustBe(TypeField.date)([ + '2023-01-01', + '2023-10-10', + ]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid date', () => { + const isValid = elementOfArrayMustBe(TypeField.date)([ + '2023-01-01', + 'not-a-date', + ]); + expect(isValid).toBe(false); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts new file mode 100644 index 00000000..50dc7040 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts @@ -0,0 +1,69 @@ +import { TypeField } from '../types'; + +export const nonEmptyObject = + () => + (object: T) => + !!(typeof object === 'object' && object && Object.keys(object).length > 0); + +export const uniqueArray = () => (e: string[]) => new Set(e).size === e.length; + +export const getValidationErrorForStrict = ( + props: string[], + name: 'Fields' | 'Filter' +) => + `Validation error: ${name} should be have only props: ["${props.join( + '","' + )}"]`; + +export const oneOf = (keys: string[]) => (val: any) => { + if (!val) return false; + for (const k of keys) { + if (val[k] !== undefined) return true; + } + return false; +}; + +export const stringLongerThan = + (length = 0) => + (str: string) => + str.length > length; + +export const arrayItemStringLongerThan = + (length = 0) => + (array: [string | null, ...(string | null)[]]) => { + const checkFunction = stringLongerThan(length); + return !array.some((i) => i !== null && !checkFunction(i)); + }; + +export const stringMustBe = + (type: TypeField = TypeField.string) => + (inputString: string | null) => { + if (inputString === null) return true; + switch (type) { + case TypeField.boolean: + return inputString === 'true' || inputString === 'false'; + case TypeField.number: + return !isNaN(+inputString); + case TypeField.date: + return new Date(inputString).toString() !== 'Invalid Date'; + default: + return true; + } + }; + +export const elementOfArrayMustBe = + (type: TypeField = TypeField.string) => + (inputArray: unknown[]) => { + const checkFunc = stringMustBe(type); + return !inputArray.some((i) => !checkFunc(`${i}`)); + }; + +export function guardIsKeyOfObject( + object: R, + key: string | number | symbol +): asserts key is keyof R { + if (typeof object === 'object' && object !== null && key in object) + return void 0; + + throw new Error('Type guard error'); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts new file mode 100644 index 00000000..96438c0d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts @@ -0,0 +1,2 @@ +export const CURRENT_DATA_SOURCE_TOKEN = Symbol('CURRENT_DATA_SOURCE_TOKEN'); +export const CURRENT_ENTITY_REPOSITORY = Symbol('CURRENT_ENTITY_REPOSITORY'); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts new file mode 100644 index 00000000..403001ad --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts @@ -0,0 +1,4 @@ +export * from './di'; + +export const SUB_QUERY_ALIAS_FOR_PAGINATION = 'subQueryWithLimitOffset'; +export const ALIAS_FOR_PAGINATION = 'aliasForPagination'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts new file mode 100644 index 00000000..32f8835f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts @@ -0,0 +1,218 @@ +import { FactoryProvider } from '@nestjs/common'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { camelToKebab, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { DataSource, EntityManager, Repository } from 'typeorm'; + +import { + CURRENT_DATA_SOURCE_TOKEN, + CURRENT_ENTITY_REPOSITORY, +} from '../constants'; +import { + CURRENT_ENTITY_MANAGER_TOKEN, + FIND_ONE_ROW_ENTITY, + CHECK_RELATION_NAME, + PARAMS_FOR_ZOD_SCHEMA, + ORM_SERVICE, + FIELD_FOR_ENTITY, + RUN_IN_TRANSACTION_FUNCTION, + GLOBAL_MODULE_OPTIONS_TOKEN, +} from '../../../constants'; +import { + EntityProps, + FieldWithType, + FindOneRowEntity, + CheckRelationNme, + ZodParams, + GetFieldForEntity, +} from '../../mixin/types'; +import { + ObjectLiteral, + EntityTarget, + ResultGeneralParam, + RequiredFromPartial, + ConfigParam, + RunInTransaction, +} from '../../../types'; +import { + getField, + getPropsTreeForRepository, + getArrayPropsForEntity, + getTypeForAllProps, + getRelationTypeArray, + getTypePrimaryColumn, + getFieldWithType, + getPropsFromDb, + getRelationTypeName, + getRelationTypePrimaryColumn, +} from '../orm-helper'; + +import { TypeOrmService, TypeormUtilsService } from '../service'; +import { getEntityName } from '../../mixin/helper'; +import { TypeOrmModule } from '@klerick/json-api-nestjs'; +import { TypeOrmParam } from '../type'; + +export function CurrentDataSourceProvider( + connectionName?: string +): FactoryProvider { + return { + provide: CURRENT_DATA_SOURCE_TOKEN, + useFactory: (dataSource: DataSource) => dataSource, + inject: [getDataSourceToken(connectionName)], + }; +} + +export function CurrentEntityManager(): FactoryProvider { + return { + provide: CURRENT_ENTITY_MANAGER_TOKEN, + useFactory: (dataSource: DataSource) => dataSource.manager, + inject: [CURRENT_DATA_SOURCE_TOKEN], + }; +} + +export function GetFieldForEntity(): FactoryProvider< + GetFieldForEntity +> { + return { + provide: FIELD_FOR_ENTITY, + useFactory: (entityManager: EntityManager) => { + return (entity: EntityTarget) => + getField(entityManager.getRepository(entity)); + }, + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + }; +} + +export function CurrentEntityRepository( + entity: E +): FactoryProvider> { + return { + provide: CURRENT_ENTITY_REPOSITORY, + useFactory: (entityManager: EntityManager) => + entityManager.getRepository(entity as unknown as EntityTarget), + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + }; +} + +export function ZodParamsFactory(): FactoryProvider< + ZodParams> +> { + return { + provide: PARAMS_FOR_ZOD_SCHEMA, + inject: [CURRENT_ENTITY_REPOSITORY], + useFactory: (repo: Repository) => { + const primaryColumns = repo.metadata.primaryColumns[0] + .propertyName as EntityProps; + const fieldWithType = ObjectTyped.entries(getFieldWithType(repo)) + .filter(([key]) => key !== repo.metadata.primaryColumns[0].propertyName) + .reduce( + (acum, [key, type]) => ({ + ...acum, + [key]: type, + }), + {} as FieldWithType + ); + + return { + entityFieldsStructure: getField(repo), + entityRelationStructure: getPropsTreeForRepository(repo), + propsArray: getArrayPropsForEntity(repo), + propsType: getTypeForAllProps(repo), + typeId: getTypePrimaryColumn(repo), + typeName: camelToKebab(getEntityName(repo.target)), + fieldWithType, + propsDb: getPropsFromDb(repo), + primaryColumn: primaryColumns, + relationArrayProps: getRelationTypeArray(repo), + relationPopsName: getRelationTypeName(repo), + primaryColumnType: getRelationTypePrimaryColumn(repo), + } satisfies ZodParams>; + }, + }; +} + +export function FindOneRowEntityFactory< + E extends ObjectLiteral +>(): FactoryProvider> { + return { + provide: FIND_ONE_ROW_ENTITY, + inject: [CURRENT_ENTITY_REPOSITORY, TypeormUtilsService], + useFactory: ( + repository: Repository, + typeormUtilsService: TypeormUtilsService + ) => { + return async (entity, value) => { + const params = 'params'; + return await repository + .createQueryBuilder(typeormUtilsService.currentAlias) + .where( + `${typeormUtilsService.getAliasPath( + typeormUtilsService.currentPrimaryColumn + )} = :${params}` + ) + .setParameters({ + [params]: value, + }) + .getOne(); + }; + }, + }; +} + +export function CheckRelationNameFactory< + E extends ObjectLiteral +>(): FactoryProvider> { + return { + provide: CHECK_RELATION_NAME, + inject: [TypeormUtilsService], + useFactory(typeormUtilsService: TypeormUtilsService) { + return (entity, value) => + !!typeormUtilsService.relationFields.find((i) => i === value); + }, + }; +} + +export function RunInTransactionFactory(): FactoryProvider { + return { + provide: RUN_IN_TRANSACTION_FUNCTION, + inject: [GLOBAL_MODULE_OPTIONS_TOKEN, CURRENT_DATA_SOURCE_TOKEN], + useFactory( + options: ResultGeneralParam & { + type: typeof TypeOrmModule; + options: RequiredFromPartial; + }, + dataSource: DataSource + ) { + const { + options: { runInTransaction }, + } = options; + + if (runInTransaction && typeof runInTransaction === 'function') { + return (callback) => + runInTransaction('READ COMMITTED', () => callback()); + } + + return async (callback) => { + const queryRunner = dataSource.createQueryRunner(); + await queryRunner.startTransaction('READ UNCOMMITTED'); + let result: unknown; + try { + result = await callback(); + await queryRunner.commitTransaction(); + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + return result; + }; + }, + }; +} + +export function OrmServiceFactory() { + return { + provide: ORM_SERVICE, + useClass: TypeOrmService, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts new file mode 100644 index 00000000..e9cdcce4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts @@ -0,0 +1,2 @@ +export * from './type-orm.module'; +export * from './type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts new file mode 100644 index 00000000..80541044 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts @@ -0,0 +1,283 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { + ObjectTyped, + EntityRelation, + TypeOfArray, + EntityProps, +} from '@klerick/json-api-nestjs-shared'; +import { Repository } from 'typeorm'; +import { IMemoryDb } from 'pg-mem'; + +import { + mockDBTestModule, + createAndPullSchemaBase, + pullUser, + pullAllData, + providerEntities, + getRepository, + Users, + Addresses, + Notes, + Comments, + Roles, + UserGroups, +} from '../../../mock-utils'; + +import { + getField, + getPropsTreeForRepository, + fromRelationTreeToArrayName, + getArrayFields, + getArrayPropsForEntity, + getFieldWithType, + getTypeForAllProps, + getRelationTypeArray, + getTypePrimaryColumn, + getPropsFromDb, + getRelationTypeName, + getRelationTypePrimaryColumn, +} from './'; + +import { PropsArray, ArrayPropsForEntity, TypeField } from '../../mixin/types'; + +describe('type-orm-helper', () => { + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + let db: IMemoryDb; + let user: Users; + let userWithRelation: Users; + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [...providerEntities(getDataSourceToken())], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + + user = await pullUser(userRepository); + userWithRelation = await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + }); + + it('getField', async () => { + const { relations, field } = getField(userRepository); + const userFieldProps = Object.getOwnPropertyNames( + user + ) as EntityProps[]; + const hasUserFieldInResultField = userFieldProps.some( + (field) => !field.includes(field) + ); + + const hasResultInUserField = field.some( + (field) => !userFieldProps.includes(field) + ); + + const userRelationProps: EntityRelation[] = ( + Object.getOwnPropertyNames(userWithRelation) as (EntityProps & + EntityRelation)[] + ).filter((props) => !userFieldProps.includes(props)); + + const hasUserRelationInResultField = userRelationProps.some( + (field) => !relations.includes(field) + ); + + const hasResultInUserRelation = relations.some( + (field) => !userRelationProps.includes(field) + ); + + expect(hasUserFieldInResultField).toEqual(false); + expect(hasResultInUserField).toEqual(false); + + expect(hasUserRelationInResultField).toEqual(false); + expect(hasResultInUserRelation).toEqual(false); + }); + + it('getPropsTreeForRepository', () => { + const relationField = getPropsTreeForRepository(userRepository); + const userFieldProps = Object.getOwnPropertyNames( + user + ) as EntityProps[]; + const userRelationProps: EntityRelation[] = ( + Object.getOwnPropertyNames(userWithRelation) as (EntityProps & + EntityRelation)[] + ).filter((props) => !userFieldProps.includes(props)); + + const hasUserRelationInResultField = userRelationProps.some( + (field) => !Object.keys(relationField).includes(field) + ); + const hasResultInUserRelation = ObjectTyped.keys(relationField).some( + (field) => !userRelationProps.includes(field) + ); + expect(hasUserRelationInResultField).toEqual(false); + expect(hasResultInUserRelation).toEqual(false); + + for (const [relationName, fieldsRelation] of ObjectTyped.entries( + relationField + )) { + const check = fieldsRelation.some((field) => { + const targetItem = userWithRelation[relationName]; + const target = Array.isArray(targetItem) ? targetItem[0] : targetItem; + // @ts-ignore + return !ObjectTyped.keys(target).includes(field); + }); + expect(check).toEqual(false); + } + }); + + it('fromRelationTreeToArrayName', () => { + const { relations, field } = getField(userRepository); + + const relationField = getPropsTreeForRepository(userRepository); + const checkArray = fromRelationTreeToArrayName(relationField); + + for (const key of relations) { + let resultKey = + key === 'manager' ? 'Users' : key === 'userGroup' ? 'UserGroups' : key; + + const relationsRepo = + userRepository.metadata.connection.getRepository< + TypeOfArray + >(resultKey); + const { field: relationsFields } = getField(relationsRepo); + const textField = relationsFields.map((r) => `${key}.${r}`); + const check = textField.some((i) => !checkArray.includes(i as any)); + expect(check).toEqual(false); + } + }); + + it('getArrayFields', () => { + const result = getArrayFields(addressesRepository); + expect(result).toEqual({ + arrayField: true, + } as PropsArray); + }); + + it('getArrayPropsForEntity', () => { + const result = getArrayPropsForEntity(userRepository); + const check: ArrayPropsForEntity = { + target: { + testReal: true, + testArrayNull: true, + }, + manager: { + testReal: true, + testArrayNull: true, + }, + comments: {}, + notes: {}, + userGroup: {}, + roles: {}, + addresses: { + arrayField: true, + }, + }; + expect(result).toEqual(check); + }); + + it('getFieldWithType', () => { + const result = getFieldWithType(addressesRepository); + expect(result.arrayField).toBe('array'); + expect(result.state).toBe('string'); + expect(result.id).toBe('number'); + expect(result.createdAt).toBe('date'); + const result2 = getFieldWithType(userRepository); + + expect(result2.isActive).toBe('boolean'); + }); + + it('getRelationType', () => { + const result = getRelationTypeArray(userRepository); + expect(result.roles).toBe(true); + expect(result.comments).toBe(true); + expect(result.manager).toBe(false); + expect(result.addresses).toBe(false); + expect(result.userGroup).toBe(false); + expect(result.notes).toBe(true); + }); + + it('getRelationTypeName', () => { + const result = getRelationTypeName(userRepository); + expect(result.roles).toBe('Roles'); + expect(result.comments).toBe('Comments'); + expect(result.manager).toBe('Users'); + expect(result.addresses).toBe('Addresses'); + expect(result.userGroup).toBe('UserGroups'); + expect(result.notes).toBe('Notes'); + }); + + it('getRelationTypePrimaryColumn', () => { + const result = getRelationTypePrimaryColumn(userRepository); + expect(result.roles).toBe(TypeField.number); + expect(result.comments).toBe(TypeField.number); + expect(result.manager).toBe(TypeField.number); + expect(result.addresses).toBe(TypeField.number); + expect(result.userGroup).toBe(TypeField.number); + expect(result.notes).toBe(TypeField.string); + }); + + it('getTypePrimaryColumn', () => { + expect(getTypePrimaryColumn(userRepository)).toBe(TypeField.number); + expect(getTypePrimaryColumn(notesRepository)).toBe(TypeField.string); + }); + // + // it('getPrimaryColumnsForRelation', () => { + // const result = getPrimaryColumnsForRelation(userRepository); + // expect(result.roles).toBe('id'); + // expect(result.manager).toBe('id'); + // }); + // + // it('geTisArrayRelation', () => { + // const result = getIsArrayRelation(userRepository); + // expect(result).toEqual({ + // addresses: false, + // manager: false, + // roles: true, + // comments: true, + // notes: true, + // userGroup: false, + // }); + // }); + + it('getTypeForAllProps', () => { + const result = getTypeForAllProps(userRepository); + expect(result.manager.id).toBe(TypeField.number); + expect(result.testDate).toBe(TypeField.date); + expect(result.comments.id).toBe(TypeField.number); + expect(result.notes.id).toBe(TypeField.string); + }); + + it('getPropsFromDb', () => { + const result = getPropsFromDb(userRepository); + // testReal has isNullable false but have default should be true + expect(result['testReal']).toEqual({ + type: 'real', + isArray: true, + isNullable: true, + }); + + const result2 = getPropsFromDb(rolesRepository); + expect(result2['key']).toEqual({ + type: 'varchar', + isArray: false, + isNullable: false, + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts new file mode 100644 index 00000000..f6e67ffa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts @@ -0,0 +1,294 @@ +import { EntityProps, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { Repository } from 'typeorm'; + +import { ObjectLiteral } from '../../../types'; +import { + RelationTree, + ValueOf, + UnionToTuple, + TypeCast, + Concat, + TypeOfArray, + CastProps, + PropsNameResultField, + PropsArray, + RelationType, + ResultGetField, + ArrayPropsForEntity, + AllFieldWithType, + TypeField, + FieldWithType, + RelationPropsArray, + TypeForId, + PropsForField, + ColumnType, + RelationPropsTypeName, + RelationPrimaryColumnType, +} from '../../mixin/types'; +import { getEntityName } from '../../mixin/helper'; + +export type ConcatFieldWithRelation< + R extends string, + T extends readonly string[] +> = ValueOf<{ + [K in T[number]]: Concat; +}>; + +export type ConcatRelationUnion< + E extends ObjectLiteral, + R = RelationTree +> = ValueOf<{ + [K in keyof R]: ConcatFieldWithRelation< + TypeCast, + TypeCast + >; +}>; + +export type ConcatRelation = TypeCast< + UnionToTuple>, + [string, ...string[]] +>; + +export const getField = ( + repository: Repository +): ResultGetField => { + const relations = repository.metadata.relations.map((i) => { + return i.propertyName; + }); + + const field = repository.metadata.columns + .filter((i) => !relations.includes(i.propertyName)) + .map((r) => r.propertyName); + + return { + field, + relations, + } as unknown as ResultGetField; +}; + +export const fromRelationTreeToArrayName = ( + relationTree: RelationTree +): ConcatRelation => { + return ObjectTyped.entries(relationTree).reduce((acum, [name, filed]) => { + acum.push(...filed.map((i) => `${name.toLocaleString()}.${i}`)); + return acum; + }, [] as string[]) as unknown as ConcatRelation; +}; + +export const getPropsTreeForRepository = ( + repository: Repository +): RelationTree => { + const dataSource = repository.metadata.connection; + + const relationType = repository.metadata.relations.reduce((acum, i) => { + acum[i.propertyName] = i.inverseEntityMetadata.target; + return acum; + }, {} as Record) as unknown as RelationType; + + return ObjectTyped.entries(relationType).reduce( + (acum, [key, value]) => ({ + ...acum, + ...{ [key]: getField(dataSource.getRepository(value))['field'] }, + }), + {} as RelationTree + ); +}; + +export type PropertyTarget< + E extends ObjectLiteral, + For extends PropsNameResultField +> = { + [K in ResultGetField[For][number]]: K extends keyof E + ? TypeOfArray + : never; +}; + +export function guardKeyForPropertyTarget< + E extends ObjectLiteral, + For extends PropsNameResultField, + R extends PropertyTarget +>(relationsTargets: R, key: any): asserts key is keyof R { + if (!(key in relationsTargets)) throw new Error('Type guard error'); +} + +export const getArrayPropsForEntity = ( + repository: Repository +): ArrayPropsForEntity => { + const connection = repository.metadata.connection; + + const relationsTargets = repository.metadata.relations.reduce( + (acum, i) => ({ + ...acum, + [i.propertyName]: i.type, + }), + {} as Record + ) as PropertyTarget; + + const { relations } = getField(repository); + const relationsArrayFields = relations.reduce( + (acum, item) => { + guardKeyForPropertyTarget(relationsTargets, item); + const target = relationsTargets[item] as TypeCast< + TypeOfArray>, + ObjectLiteral + >; + + const repository = connection.getRepository( + target as Function + ) as Repository; + + acum[item] = getArrayFields(repository) as PropsArray< + TypeOfArray> + >; + + return acum; + }, + {} as { + [K in ResultGetField['relations'][number]]: PropsArray< + TypeOfArray> + >; + } + ); + + return { + target: getArrayFields(repository), + ...relationsArrayFields, + }; +}; + +export const getArrayFields = ( + repository: Repository +): PropsArray => { + const relations = repository.metadata.relations.map((i) => { + return i.propertyName; + }); + + return repository.metadata.columns + .filter((i) => !relations.includes(i.propertyName)) + .reduce((acum, metaData) => { + if (metaData.isArray) { + acum[metaData.propertyName] = true; + } + return acum; + }, {} as Record) as PropsArray; +}; + +export const getTypeForAllProps = ( + repository: Repository +): AllFieldWithType => { + const targetField = getFieldWithType(repository); + + const relationField = repository.metadata.relations.reduce((acum, item) => { + acum[item.propertyName] = getFieldWithType( + repository.manager.getRepository(item.inverseEntityMetadata.target) + ); + return acum; + }, {} as any); + + return { + ...targetField, + ...relationField, + }; +}; + +export const getFieldWithType = ( + repository: Repository +): FieldWithType => { + const { field } = getField(repository); + + const entity = repository.target as any; + const result = {} as any; + for (const item of field) { + let typeProps: TypeField = TypeField.string; + switch (Reflect.getMetadata('design:type', entity['prototype'], item)) { + case Array: + typeProps = TypeField.array; + break; + case Date: + typeProps = TypeField.date; + break; + case Number: + typeProps = TypeField.number; + break; + case Boolean: + typeProps = TypeField.boolean; + break; + case Object: + typeProps = TypeField.object; + break; + default: + typeProps = TypeField.string; + } + result[item] = typeProps; + } + + return result; +}; + +export const getRelationTypeArray = ( + repository: Repository +): RelationPropsArray => { + const { relations } = getField(repository); + + const entity = repository.target as any; + const result = {} as any; + for (const item of relations) { + result[item] = + Reflect.getMetadata('design:type', entity['prototype'], item) === Array; + } + return result; +}; + +export const getTypePrimaryColumn = ( + repository: Repository +): TypeForId => { + const target = repository.target as any; + const primaryColumn = repository.metadata.primaryColumns[0].propertyName; + + return Reflect.getMetadata( + 'design:type', + target['prototype'], + primaryColumn + ) === Number + ? TypeField.number + : TypeField.string; +}; + +export const getPropsFromDb = ( + repository: Repository +): PropsForField => { + return repository.metadata.columns.reduce((acum, i) => { + const tmp = i.propertyName as unknown as EntityProps; + acum[tmp] = { + type: i.type as ColumnType, + isArray: i.isArray, + isNullable: i.isNullable || i.default !== undefined, + }; + return acum; + }, {} as PropsForField); +}; + +export const getRelationTypeName = ( + repository: Repository +): RelationPropsTypeName => { + return repository.metadata.relations.reduce((acum, i) => { + acum[i.propertyName] = getEntityName(i.inverseEntityMetadata.target); + return acum; + }, {} as Record) as RelationPropsTypeName; +}; + +export const getRelationTypePrimaryColumn = ( + repository: Repository +): RelationPrimaryColumnType => { + return repository.metadata.relations.reduce((acum, i) => { + const target = i.inverseEntityMetadata.target as any; + const primaryColumn = + i.inverseEntityMetadata.primaryColumns[0].propertyName; + acum[i.propertyName] = + Reflect.getMetadata('design:type', target['prototype'], primaryColumn) === + Number + ? TypeField.number + : TypeField.string; + return acum; + }, {} as Record) as RelationPrimaryColumnType; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts new file mode 100644 index 00000000..4f730de9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts @@ -0,0 +1,71 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; + +import { + createAndPullSchemaBase, + mockDBTestModule, + providerEntities, +} from '../../../../mock-utils'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; + +import { getRepository, pullUser, Users } from '../../../../mock-utils'; + +import { Repository } from 'typeorm'; +import { CONTROL_OPTIONS_TOKEN, ORM_SERVICE } from '../../../../constants'; + +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('deleteOne', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let user: Users; + let userRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ userRepository } = getRepository(module)); + user = await pullUser(userRepository); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + }); + + it('Should be ok', async () => { + await typeormService.deleteOne(`${user.id}`); + expect(await userRepository.findOneBy({ id: user.id })).toBe(null); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts new file mode 100644 index 00000000..1a6871d7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts @@ -0,0 +1,21 @@ +import { FindOptionsWhere } from 'typeorm'; +import { TypeOrmService } from '../../service'; +import { ObjectLiteral } from '../../../../types'; + +export async function deleteOne( + this: TypeOrmService, + id: number | string +): Promise { + const data = await this.repository.findOne({ + where: { + [this.typeormUtilsService.currentPrimaryColumn.toString()]: id, + } as FindOptionsWhere, + }); + if (!data) return void 0; + + this.config.useSoftDelete + ? await this.repository.softRemove(data) + : await this.repository.remove(data); + + return void 0; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts new file mode 100644 index 00000000..5a83d3fc --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts @@ -0,0 +1,190 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; + +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('deleteRelationship', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + let typeormUtilsService: TypeormUtilsService; + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + typeormUtilsService = + module.get>(TypeormUtilsService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Should be ok', async () => { + const checkUser = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + + const roles = await rolesRepository.find(); + const userGroups = await userGroupRepository.find(); + const users = await userRepository.find(); + + if (!checkUser) { + throw new Error('not found mock'); + } + + const userGroupData = { + type: 'user-groups', + id: userGroups + .find((i) => checkUser.userGroup.id === i.id) + ?.id.toString(), + }; + const rolesData = [ + { + type: 'roles', + id: roles + .find((i) => checkUser.roles.find((a) => a.id === i.id)) + ?.id.toString(), + }, + ]; + + const managerData = { + type: 'users', + id: users.find((i) => checkUser.manager.id === i.id)?.id.toString(), + }; + await typeormService.deleteRelationship(1, 'roles', rolesData as any); + await typeormService.deleteRelationship( + 1, + 'userGroup', + userGroupData as any + ); + await typeormService.deleteRelationship(1, 'manager', managerData as any); + + const checkUserAfterPost = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + if (!checkUserAfterPost) { + throw new Error('not found'); + } + expect(checkUserAfterPost.manager).toBe(null); + expect(checkUserAfterPost.roles.map((i) => i.id.toString()).sort()).toEqual( + checkUser.roles + .map((i) => i.id.toString()) + .filter((i) => !rolesData.map((i) => i.id).includes(i)) + .sort() + ); + expect(checkUserAfterPost.userGroup).toBe(null); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts new file mode 100644 index 00000000..141b41a8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts @@ -0,0 +1,32 @@ +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; + +import { PostRelationshipData } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; + +export async function deleteRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: TypeOrmService, + id: number | string, + rel: Rel, + input: PostRelationshipData +): Promise { + const idsResult = await this.typeormUtilsService.validateRelationInputData( + rel, + input + ); + const postBuilder = this.repository + .createQueryBuilder() + .relation(rel.toString()) + .of(id); + + if (Array.isArray(idsResult)) { + await postBuilder.remove(idsResult); + } else { + await postBuilder.set(null); + } + return void 0; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts new file mode 100644 index 00000000..45500dc3 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts @@ -0,0 +1,406 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { Equal, IsNull, Repository } from 'typeorm'; +import { IMemoryDb } from 'pg-mem'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, + DEFAULT_QUERY_PAGE, + DEFAULT_PAGE_SIZE, +} from '../../../../constants'; +import { ObjectLiteral as Entity } from '../../../../types'; + +import { Query } from '../../../mixin/zod'; + +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +function getDefaultQuery() { + const filter = { + relation: null, + target: null, + }; + const defaultQuery: Query = { + [QueryField.filter]: filter, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }, + }; + + return defaultQuery; +} + +describe('getAll', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('order', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + + const checkData = await userRepository.find({ + relations: { + addresses: true, + comments: true, + }, + order: { + id: 'DESC', + comments: { + id: 'DESC', + }, + }, + }); + + const query = getDefaultQuery(); + query.include = ['addresses', 'comments']; + query.sort = { + target: { + id: 'DESC', + }, + comments: { + id: 'DESC', + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith(checkData); + }); + + it('include', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + + const checkData = await userRepository.findOne({ + where: { + id: 1, + }, + relations: { + addresses: true, + comments: true, + }, + }); + + const query = getDefaultQuery(); + query.include = ['addresses', 'comments']; + query.filter.target = { + id: { + eq: `${checkData?.id}`, + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + it('select', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + + const checkData = await userRepository.findOne({ + select: { + id: true, + isActive: true, + addresses: { + state: true, + id: true, + }, + comments: { + text: true, + id: true, + }, + }, + where: { + id: 1, + }, + relations: { + addresses: true, + comments: true, + }, + }); + + const query = getDefaultQuery(); + query.fields = { + target: ['id', 'isActive'], + addresses: ['state'], + comments: ['text'], + }; + query.include = ['addresses', 'comments']; + query.filter.target = { + id: { + eq: `${checkData?.id}`, + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + describe('filter', () => { + let firstRole: Roles; + let secondRole: Roles; + let addresses: Addresses[]; + let comments: Comments[]; + beforeAll(async () => { + firstRole = (await rolesRepository.findOneBy({ + id: 1, + })) as Roles; + secondRole = (await rolesRepository.findOneBy({ + id: 2, + })) as Roles; + + addresses = await addressesRepository.find(); + comments = await commentsRepository.find(); + }); + + it('Target props with null', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + + const query = getDefaultQuery(); + query.filter.target = { + id: { eq: '1' }, + firstName: { eq: null }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toHaveBeenCalledTimes(0); + }); + + it('Target props', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.findOne({ + where: { + id: 1, + }, + }); + const query = getDefaultQuery(); + query.filter.target = { + id: { eq: `${checkData?.id}` }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + it('Check relation with the same Entity', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.findOne({ + where: { + id: 1, + comments: { + text: Equal(comments[0].text), + }, + }, + relations: { + comments: true, + }, + }); + const query = getDefaultQuery(); + query.filter.relation = { + comments: { + text: { + eq: comments[0].text, + }, + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + // it('Target relation is null', async () => { + // const query = getDefaultQuery(); + // query.filter.target = { + // comments: { + // eq: 'null', + // }, + // }; + // await typeormService.getAll(query); + // }); + + it('Relation many-to-one', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.findOne({ + where: { + id: 1, + }, + relations: { + manager: true, + }, + }); + + const query = getDefaultQuery(); + query.filter.target = { + id: { + eq: '1', + }, + }; + query.filter.relation = { + manager: { + id: { + eq: '2', + }, + }, + }; + query.include = ['manager']; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + it('Relation one-to-many', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.findOne({ + where: { + id: 1, + addresses: { + state: Equal(addresses[0].state), + }, + }, + relations: { + addresses: true, + }, + }); + const query = getDefaultQuery(); + query.filter.relation = { + addresses: { + state: { + eq: addresses[0].state, + }, + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + it('Relation many-to-many', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.find({ + where: { + id: 1, + roles: { + name: Equal(firstRole.name), + }, + }, + relations: { + roles: true, + }, + }); + + const query = getDefaultQuery(); + query.include = ['roles']; + query.filter.relation = { + roles: { + name: { + eq: firstRole.name, + }, + }, + }; + const { data } = await typeormService.getAll(query); + expect(spyOnTransformData).not.toBeCalled(); + expect(data).toEqual([]); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts new file mode 100644 index 00000000..2fe92c5e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts @@ -0,0 +1,264 @@ +import { ObjectTyped, ResourceObject } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; +import { Query } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; +import { TupleOfEntityRelation } from '../../../mixin/types'; +import { ASC, DESC } from '../../../../constants'; + +import { + ALIAS_FOR_PAGINATION, + SUB_QUERY_ALIAS_FOR_PAGINATION, +} from '../../constants'; + +type OrderByCondition = Record; + +function getSortObject(params: any, relationName: string) { + return Object.entries(params).reduce((acum, [props, sort]) => { + acum[`${relationName}.${props}`] = `${sort}` === ASC ? ASC : DESC; + return acum; + }, {} as OrderByCondition); +} + +export async function getAll( + this: TypeOrmService, + query: Query +): Promise> { + const { fields, filter, include, sort, page } = query; + + let defaultSortObject: OrderByCondition = { + [`${ + this.typeormUtilsService.currentAlias + }.${this.typeormUtilsService.currentPrimaryColumn.toString()}`]: ASC, + }; + + const includeForCountQuery = new Set(); + const selectFields = new Set(); + const includeRel = new Set(); + + const skip = (page.number - 1) * page.size; + + const expressionArrayForTarget = + this.typeormUtilsService.getFilterExpressionForTarget(query); + const expressionArrayForRelation = + this.typeormUtilsService.getFilterExpressionForRelation(query); + const expressionArray = [ + ...expressionArrayForTarget, + ...expressionArrayForRelation, + ]; + + if (sort) { + const { target, ...relation } = sort; + const targetOrder = getSortObject( + target || {}, + this.typeormUtilsService.currentAlias + ); + + const relOrder = Object.entries(relation || {}).reduce( + (acum, [name, order]) => { + return { + ...acum, + ...getSortObject( + order || {}, + this.typeormUtilsService.getAliasForRelation(name) + ), + }; + }, + {} as OrderByCondition + ); + const resultOrder = { + ...targetOrder, + ...relOrder, + }; + if (Object.keys(resultOrder).length > 0) { + defaultSortObject = resultOrder; + } + for (const item of ObjectTyped.keys(relation)) { + includeForCountQuery.add(item.toString()); + } + } + + const queryBuilderForCount = this.repository + .createQueryBuilder(this.typeormUtilsService.currentAlias) + .select( + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + ), + this.typeormUtilsService.currentPrimaryColumn.toString() + ) + .orderBy(defaultSortObject); + + for (const i in expressionArray) { + const { params, alias, selectInclude, expression } = expressionArray[i]; + const expressionTempArray: string[] = []; + if (alias) { + expressionTempArray.push(alias); + } + expressionTempArray.push(expression); + queryBuilderForCount[i === '0' ? 'where' : 'andWhere']( + expressionTempArray.join(' ') + ); + if (params) { + if (Array.isArray(params)) { + for (const { name, val } of params) { + queryBuilderForCount.setParameters({ [name]: val }); + } + } else { + queryBuilderForCount.setParameters({ [params.name]: params.val }); + } + } + if (selectInclude) includeForCountQuery.add(selectInclude); + } + + for (const rel of [...includeForCountQuery]) { + const currentIncludeAlias = + this.typeormUtilsService.getAliasForRelation(rel); + queryBuilderForCount.leftJoin( + this.typeormUtilsService.getAliasPath(rel), + currentIncludeAlias + ); + } + + const count = await queryBuilderForCount.getCount(); + const meta = { + pageNumber: page.number, + totalItems: count, + pageSize: page.size, + }; + + if (count === 0) { + return { + meta, + data: [], + }; + } + + const aliasForIdResultPagination = this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn, + ALIAS_FOR_PAGINATION, + '_' + ); + + const resultIds = await this.repository + .createQueryBuilder(ALIAS_FOR_PAGINATION) + .select( + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn, + ALIAS_FOR_PAGINATION + ), + aliasForIdResultPagination + ) + .innerJoin( + `(${queryBuilderForCount.offset(skip).limit(page.size).getQuery()})`, + SUB_QUERY_ALIAS_FOR_PAGINATION, + `${this.typeormUtilsService.getAliasPath( + queryBuilderForCount.escape( + this.typeormUtilsService.currentPrimaryColumn.toString() + ), + queryBuilderForCount.escape(SUB_QUERY_ALIAS_FOR_PAGINATION) + )} = ${this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn, + ALIAS_FOR_PAGINATION + )}` + ) + .setParameters(queryBuilderForCount.getParameters()) + .getRawMany<{ + [K: typeof aliasForIdResultPagination]: number; + }>(); + + const ids = resultIds.map((i) => i[aliasForIdResultPagination]); + if (ids.length === 0) { + return { + meta, + data: [], + }; + } + if (include) { + for (const rel of include) { + includeRel.add(rel); + } + } + + if (fields) { + if (include) { + for (const rel of include) { + const currentIncludeAlias = + this.typeormUtilsService.getAliasForRelation(rel); + const primaryColumnName = + this.typeormUtilsService.getPrimaryColumnForRel(rel); + selectFields.add(`${currentIncludeAlias}.${primaryColumnName}`); + } + } + + const { target, ...other } = fields; + if (target) { + for (const item of target) { + selectFields.add(`${this.typeormUtilsService.currentAlias}.${item}`); + } + } + + for (const [rel, fields] of ObjectTyped.entries(other)) { + const currentIncludeAlias = this.typeormUtilsService.getAliasForRelation( + rel as TupleOfEntityRelation[number] + ); + if (!fields) continue; + for (const field of fields) { + selectFields.add(`${currentIncludeAlias.toString()}.${field}`); + } + } + } + + const resultQuery = this.repository + .createQueryBuilder() + .orderBy(defaultSortObject); + + if (selectFields.size > 0) { + resultQuery.select([...selectFields]); + } + + resultQuery.whereInIds(ids); + for (const expressionItem of expressionArrayForRelation) { + const { selectInclude, alias, paramsForResult, params, expression } = + expressionItem; + if (paramsForResult) { + for (const item of paramsForResult) { + resultQuery.andWhere(item); + } + } else { + resultQuery.andWhere(`${alias} ${expression}`); + } + + if (params) { + if (Array.isArray(params)) { + for (const item of params) { + resultQuery.setParameters({ [item.name]: item.val }); + } + } else { + resultQuery.setParameters({ [params.name]: params.val }); + } + } + if (selectInclude) includeRel.add(selectInclude); + } + + for (const item of [...includeRel]) { + const currentIncludeAlias = + this.typeormUtilsService.getAliasForRelation(item); + if (!currentIncludeAlias) continue; + resultQuery[selectFields.size > 0 ? 'leftJoin' : 'leftJoinAndSelect']( + this.typeormUtilsService.getAliasPath(item), + currentIncludeAlias + ); + } + const resultData = await resultQuery.getMany(); + const { included, data } = + this.transformDataService.transformData(resultData); + return { + meta: { + pageNumber: page.number, + totalItems: count, + pageSize: page.size, + }, + data, + ...(included ? { included } : {}), + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts new file mode 100644 index 00000000..d0a6adaa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts @@ -0,0 +1,191 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { IMemoryDb } from 'pg-mem'; +import { Equal, Repository } from 'typeorm'; + +import { ObjectLiteral as Entity } from '../../../../types'; +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + DEFAULT_PAGE_SIZE, + DEFAULT_QUERY_PAGE, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { Query } from '../../../mixin/zod'; +import { NotFoundException } from '@nestjs/common'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +function getDefaultQuery() { + const defaultQuery: Query = { + [QueryField.filter]: { + relation: null, + target: null, + }, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }, + }; + + return defaultQuery; +} + +describe('getOne', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Get one item', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const query = getDefaultQuery(); + const checkData = await userRepository.findOne({ + where: { + id: Equal(1), + }, + relations: { + addresses: true, + comments: true, + }, + }); + query.include = ['addresses', 'comments']; + await typeormService.getOne('1', query); + expect(spyOnTransformData).toBeCalledWith(checkData); + }); + it('Get one item with select', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const query = getDefaultQuery(); + const checkData = await userRepository.findOne({ + select: { + firstName: true, + id: true, + isActive: true, + comments: { + id: true, + text: true, + }, + addresses: { + id: true, + }, + manager: { + id: true, + login: true, + }, + }, + where: { + id: Equal(1), + }, + relations: { + addresses: true, + comments: true, + manager: true, + }, + }); + query.include = ['addresses', 'comments', 'manager']; + query.fields = { + target: ['firstName', 'isActive'], + comments: ['text'], + manager: ['login'], + }; + await typeormService.getOne('1', query); + expect(spyOnTransformData).toBeCalledWith(checkData); + }); + it('Should be error', async () => { + expect.assertions(1); + try { + const query = getDefaultQuery(); + await typeormService.getOne('1000000', query); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts new file mode 100644 index 00000000..257ef7cb --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts @@ -0,0 +1,95 @@ +import { NotFoundException } from '@nestjs/common'; +import { ObjectTyped, ResourceObject } from '@klerick/json-api-nestjs-shared'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { QueryOne } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; + +export async function getOne( + this: TypeOrmService, + id: number | string, + query: QueryOne +): Promise> { + const { include, fields } = query; + const selectFields = new Set(); + const builder = this.repository.createQueryBuilder( + this.typeormUtilsService.currentAlias + ); + + if (fields) { + selectFields.add( + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + ) + ); + + const { target, ...other } = fields; + if (target) { + for (const fieldItem of target) { + selectFields.add(this.typeormUtilsService.getAliasPath(fieldItem)); + } + } + + for (const [rel, fieldRel] of ObjectTyped.entries(other)) { + if (fieldRel) { + for (const itemFieldRel of fieldRel) { + selectFields.add( + this.typeormUtilsService.getAliasPath( + itemFieldRel, + this.typeormUtilsService.getAliasForRelation(rel.toString()) + ) + ); + } + } + } + } + + if (include) { + for (const rel of include) { + const currentIncludeAlias = + this.typeormUtilsService.getAliasForRelation(rel); + + builder[fields ? 'leftJoin' : 'leftJoinAndSelect']( + this.typeormUtilsService.getAliasPath(rel), + currentIncludeAlias + ); + + if (fields) { + selectFields.add( + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.getPrimaryColumnForRel(rel), + currentIncludeAlias + ) + ); + } + } + } + if (selectFields.size > 0) { + builder.select([...selectFields]); + } + const paramsId = 'paramsId'; + const result = await builder + .where( + `${this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + )} = :${paramsId}`, + { + [paramsId]: id, + } + ) + .getOne(); + + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + const { included, data } = this.transformDataService.transformData(result); + return { + meta: {}, + data, + ...(included ? { included } : {}), + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts new file mode 100644 index 00000000..161d1f83 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts @@ -0,0 +1,131 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; + +import { NotFoundException } from '@nestjs/common'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('getRelationship', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Should be ok', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'getRelationships' + ); + const id = 1; + const rel = 'roles'; + const check = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + }, + where: { id }, + relations: { + roles: true, + }, + }); + const result = await typeormService.getRelationship(id, rel); + expect(spyOnTransformData).toBeCalledWith(check, rel); + expect(result).toHaveProperty('data'); + }); + it('Should be error', async () => { + expect.assertions(1); + try { + await typeormService.getRelationship('1000000', 'roles'); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts new file mode 100644 index 00000000..d50f45a9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts @@ -0,0 +1,71 @@ +import { + EntityRelation, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; + +import { + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { TypeOrmService } from '../../service'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; + +export async function getRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: TypeOrmService, + id: number | string, + rel: Rel +): Promise> { + const paramsId = 'paramsId'; + const result = await this.repository + .createQueryBuilder() + .select([ + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + ), + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.getPrimaryColumnForRel(rel.toString()), + this.typeormUtilsService.getAliasForRelation(rel.toString()) + ), + ]) + .where( + ` + ${this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + )} = :paramsId + ` + ) + .leftJoin( + this.typeormUtilsService.getAliasPath(rel.toString()), + this.typeormUtilsService.getAliasForRelation(rel.toString()) + ) + .setParameters({ + [paramsId]: id, + }) + .getOne(); + + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + + const { data } = this.transformDataService.getRelationships(result, rel); + if (data === undefined) { + const error: ValidateQueryError = { + code: 'custom', + message: `transformDataService.getRelationships return undefined`, + path: [], + }; + throw new InternalServerErrorException([error]); + } + return { + meta: {}, + data, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts new file mode 100644 index 00000000..4de29ce2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts @@ -0,0 +1,21 @@ +export { getAll } from './get-all/get-all'; +export { getOne } from './get-one/get-one'; +export { deleteOne } from './delete-one/delete-one'; +export { postOne } from './post-one/post-one'; +export { patchOne } from './patch-one/patch-one'; +export { getRelationship } from './get-relationship/get-relationship'; +export { postRelationship } from './post-relationship/post-relationship'; +export { deleteRelationship } from './delete-relationship/delete-relationship'; +export { patchRelationship } from './patch-relationship/patch-relationship'; + +// export const MethodsService = { +// getAll, +// getOne, +// deleteOne, +// postOne, +// patchOne, +// getRelationship, +// postRelationship, +// deleteRelationship, +// patchRelationship, +// }; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts new file mode 100644 index 00000000..9cc08057 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts @@ -0,0 +1,308 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IBackup, IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; + +import { PatchData, PostData } from '../../../mixin/zod'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('patchOne', () => { + let db: IMemoryDb; + let backaUp: IBackup; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + const firstName = 'firstName test'; + const isActive = false; + const testDate = new Date(); + const login = 'login test'; + + let inputData: PostData; + let newData: PatchData; + + let notes: Notes[]; + let users: Users[]; + let roles: Roles[]; + let userGroup: UserGroups[]; + let addresses: Addresses[]; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + + notes = await notesRepository.find(); + users = await userRepository.find(); + roles = await rolesRepository.find(); + userGroup = await userGroupRepository.find({ + relations: { + users: true, + }, + }); + addresses = await addressesRepository.find(); + + inputData = { + type: 'users', + attributes: { + firstName, + isActive, + testDate, + login, + }, + relationships: { + addresses: { + data: { + type: 'addresses', + id: addresses[0].id.toString(), + }, + }, + notes: { + data: [ + { + type: 'notes', + id: notes[0].id, + }, + ], + }, + roles: { + data: [ + { + type: 'roles', + id: `${roles[0].id}`, + }, + ], + }, + manager: { + data: { + type: 'users', + id: `${users[0].id}`, + }, + }, + userGroup: { + data: { + type: 'user-group', + id: `${userGroup[0].id}`, + }, + }, + }, + }; + + await typeormService.postOne(inputData); + backaUp = db.backup(); + const changeUser = await userRepository.findOneBy({ + login: inputData.attributes.login as string, + }); + if (!changeUser) { + throw new Error('not found mock data'); + } + newData = { + ...inputData, + id: `${changeUser.id}`, + }; + const newLogin = `${changeUser.login} - newLogin`; + const newIsActive = !changeUser.isActive; + + newData.attributes.login = newLogin; + newData.attributes.isActive = newIsActive; + newData.attributes.testDate = new Date(); + + newData.relationships = { + ...newData.relationships, + manager: { + data: { + type: 'users', + id: users[1].id.toString(), + }, + }, + addresses: { + data: null, + }, + userGroup: { + data: { + type: 'user-group', + id: `${userGroup[1].id}`, + }, + }, + roles: { + data: [ + { + type: 'roles', + id: `${roles[1].id}`, + }, + ], + }, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + backaUp.restore(); + }); + + it('should be ok without relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + })); + + const { relationships, ...withoutRelationships } = newData; + const returnData = await typeormService.patchOne( + withoutRelationships.id as string, + withoutRelationships + ); + + const result = await userRepository.findOneBy({ + id: parseInt(withoutRelationships.id as string, 10), + }); + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).not.toHaveProperty('included'); + }); + + it('should be ok with relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + included: {} as any, + })); + + const returnData = await typeormService.patchOne( + newData.id as string, + newData + ); + + const result = await userRepository.findOne({ + where: { + id: parseInt(newData.id as string, 10), + }, + relations: { + addresses: true, + notes: true, + userGroup: true, + roles: true, + manager: true, + }, + }); + + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).toHaveProperty('included'); + }); + + it('should be ok with relation nulling relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + included: {} as any, + })); + + newData.relationships = { + ...newData.relationships, + userGroup: { + data: null, + }, + roles: { + data: [], + }, + }; + + const returnData = await typeormService.patchOne( + newData.id as string, + newData + ); + + const result = await userRepository.findOne({ + where: { + id: parseInt(newData.id as string, 10), + }, + relations: { + addresses: true, + notes: true, + userGroup: true, + roles: true, + manager: true, + }, + }); + + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).toHaveProperty('included'); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts new file mode 100644 index 00000000..90b57273 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts @@ -0,0 +1,73 @@ +import { + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { DeepPartial } from 'typeorm'; +import { ResourceObject, ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { PatchData } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; + +export async function patchOne( + this: TypeOrmService, + id: number | string, + inputData: PatchData +): Promise> { + const { id: idBody, attributes, relationships } = inputData; + + if (`${id}` !== idBody) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Data 'id' must be equal to url param`, + path: ['data', 'id'], + }; + + throw new UnprocessableEntityException([error]); + } + + const paramsId = 'paramsId'; + const result = await this.repository + .createQueryBuilder() + .where( + `${this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + )} = :${paramsId}`, + { + [paramsId]: id, + } + ) + .getOne(); + + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, + path: ['data', 'id'], + }; + throw new NotFoundException([error]); + } + + if (attributes) { + const entityTarget = this.repository.manager.create( + this.repository.target, + attributes as DeepPartial + ); + for (const [props, val] of ObjectTyped.entries(entityTarget)) { + result[props] = val; + } + } + + const saveData = await this.typeormUtilsService.saveEntityData( + result, + relationships + ); + + const { data, included } = this.transformDataService.transformData(saveData); + const includeData = included ? { included } : {}; + return { + meta: {}, + data, + ...includeData, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts new file mode 100644 index 00000000..2cfe5343 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts @@ -0,0 +1,230 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('patchRelationship', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + let typeormUtilsService: TypeormUtilsService; + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + typeormUtilsService = + module.get>(TypeormUtilsService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Should be ok', async () => { + const checkUser = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + + const roles = await rolesRepository.find(); + const userGroups = await userGroupRepository.find(); + const users = await userRepository.find(); + + if (!checkUser) { + throw new Error('not found mock'); + } + + const userGroupData = { + type: 'user-groups', + id: userGroups + .find((i) => checkUser.userGroup.id !== i.id) + ?.id.toString(), + }; + const rolesData = [ + { + type: 'roles', + id: roles + .find((i) => checkUser.roles.find((a) => a.id !== i.id)) + ?.id.toString(), + }, + ]; + + const managerData = { + type: 'users', + id: users.find((i) => checkUser.manager.id !== i.id)?.id.toString(), + }; + const result = await typeormService.patchRelationship( + 1, + 'roles', + rolesData as any + ); + const result1 = await typeormService.patchRelationship( + 1, + 'userGroup', + userGroupData as any + ); + const result2 = await typeormService.patchRelationship( + 1, + 'manager', + managerData as any + ); + + const checkUserAfterPost = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + if (!checkUserAfterPost) { + throw new Error('not found'); + } + expect(checkUserAfterPost.manager.id.toString()).toBe(managerData.id); + expect(checkUserAfterPost.roles.map((i) => i.id.toString())).toEqual( + rolesData.map((i) => i.id) + ); + expect(checkUserAfterPost.userGroup.id.toString()).toBe(userGroupData.id); + + await typeormService.patchRelationship(1, 'roles', []); + await typeormService.patchRelationship(1, 'manager', null); + const checkUserAfterPatch = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + if (!checkUserAfterPatch) { + throw new Error('not found'); + } + + expect(checkUserAfterPatch.manager).toBe(null); + expect(checkUserAfterPatch.roles).toEqual([]); + expect(result.data.map((i) => i.id)).toEqual( + checkUserAfterPost.roles.map((i) => i.id.toString()) + ); + expect(result2.data?.id).toEqual(checkUserAfterPost.manager.id.toString()); + expect(result1.data?.id).toEqual( + checkUserAfterPost.userGroup.id.toString() + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts new file mode 100644 index 00000000..c8ed4f81 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts @@ -0,0 +1,51 @@ +import { + EntityRelation, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; + +import { PatchRelationshipData } from '../../../mixin/zod'; +import { getRelationship } from '../get-relationship/get-relationship'; +import { TypeOrmService } from '../../service'; + +export async function patchRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: TypeOrmService, + id: number | string, + rel: Rel, + input: PatchRelationshipData +): Promise> { + const idsResult = await this.typeormUtilsService.validateRelationInputData( + rel, + input + ); + + const patchBuilder = this.repository + .createQueryBuilder() + .relation(rel.toString()) + .of(id); + + if (Array.isArray(idsResult)) { + const data = await getRelationship.call< + TypeOrmService, + [number | string, Rel], + Promise> + >(this, id, rel); + const idsToDelete = Array.isArray(data.data) + ? data.data.map((i) => i.id) + : []; + + await patchBuilder.addAndRemove(idsResult, idsToDelete); + } else { + await patchBuilder.set(idsResult); + } + + return getRelationship.call< + TypeOrmService, + [number | string, Rel], + Promise> + >(this, id, rel); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts new file mode 100644 index 00000000..a2e45206 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts @@ -0,0 +1,256 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IBackup, IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + Pods, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; + +import { PostData } from '../../../mixin/zod'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('postOne', () => { + let db: IMemoryDb; + let backaUp: IBackup; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + let podsRepository: Repository; + + let typeormServicePods: TypeOrmService; + let transformDataServicePods: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + const firstName = 'firstName test'; + const isActive = false; + const testDate = new Date(); + const login = 'login test'; + + let inputData: PostData; + + let notes: Notes[]; + let users: Users[]; + let roles: Roles[]; + let userGroup: UserGroups[]; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + + const modulePods: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Pods), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + podsRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + backaUp = db.backup(); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + + typeormServicePods = modulePods.get>(ORM_SERVICE); + transformDataServicePods = + modulePods.get>(TransformDataService); + + notes = await notesRepository.find(); + users = await userRepository.find(); + roles = await rolesRepository.find(); + userGroup = await userGroupRepository.find(); + + inputData = { + type: 'users', + attributes: { + firstName, + isActive, + testDate, + login, + }, + relationships: { + notes: { + data: [ + { + type: 'notes', + id: notes[0].id, + }, + ], + }, + roles: { + data: [ + { + type: 'roles', + id: `${roles[0].id}`, + }, + ], + }, + manager: { + data: { + type: 'users', + id: `${users[0].id}`, + }, + }, + userGroup: { + data: { + type: 'user-group', + id: `${userGroup[0].id}`, + }, + }, + }, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + backaUp.restore(); + }); + + it('should be ok without relation and with id', async () => { + const spyOnTransformData = jest + .spyOn(transformDataServicePods, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + })); + const { relationships, ...other } = inputData; + const id = '5'; + const returnData = await typeormServicePods.postOne({ + id, + type: 'pods', + attributes: { + name: 'test', + }, + }); + const result = await podsRepository.findOneBy({ + id, + }); + + expect(spyOnTransformData).toBeCalledWith({ + ...result, + id, + }); + expect(returnData).not.toHaveProperty('included'); + }); + + it('should be ok without relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + })); + const { relationships, ...other } = inputData; + const returnData = await typeormService.postOne(other); + const result = await userRepository.findOneBy({ + login, + }); + + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).not.toHaveProperty('included'); + }); + + it('should be ok with relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + included: {} as any, + })); + const returnData = await typeormService.postOne(inputData); + const result = await userRepository.findOne({ + where: { + login, + }, + relations: { + notes: true, + userGroup: true, + roles: true, + manager: true, + }, + }); + + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).toHaveProperty('included'); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts new file mode 100644 index 00000000..d4decd3b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts @@ -0,0 +1,39 @@ +import { DeepPartial } from 'typeorm'; +import { ResourceObject } from '@klerick/json-api-nestjs-shared'; +import { ObjectLiteral } from '../../../../types'; +import { PostData } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; + +export async function postOne( + this: TypeOrmService, + inputData: PostData +): Promise> { + const { attributes, relationships, id } = inputData; + + const idObject = id + ? { [this.typeormUtilsService.currentPrimaryColumn.toString()]: id } + : {}; + + const attributesObject = { + ...attributes, + ...idObject, + } as DeepPartial; + + const entityTarget = this.repository.manager.create( + this.repository.target, + attributesObject + ); + + const saveData = await this.typeormUtilsService.saveEntityData( + entityTarget, + relationships + ); + + const { data, included } = this.transformDataService.transformData(saveData); + const includeData = included ? { included } : {}; + return { + meta: {}, + data, + ...includeData, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts new file mode 100644 index 00000000..a1126f32 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts @@ -0,0 +1,205 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('postRelationship', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + let typeormUtilsService: TypeormUtilsService; + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + typeormUtilsService = + module.get>(TypeormUtilsService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Should be ok', async () => { + const checkUser = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + + const roles = await rolesRepository.find(); + const userGroups = await userGroupRepository.find(); + const users = await userRepository.find(); + + if (!checkUser) { + throw new Error('not found mock'); + } + + const userGroupData = { + type: 'user-groups', + id: userGroups + .find((i) => checkUser.userGroup.id !== i.id) + ?.id.toString(), + }; + const rolesData = [ + { + type: 'roles', + id: roles + .find((i) => checkUser.roles.find((a) => a.id !== i.id)) + ?.id.toString(), + }, + ]; + + const managerData = { + type: 'users', + id: users.find((i) => checkUser.manager.id !== i.id)?.id.toString(), + }; + const result = await typeormService.postRelationship( + 1, + 'roles', + rolesData as any + ); + const result1 = await typeormService.postRelationship( + 1, + 'userGroup', + userGroupData as any + ); + + const result2 = await typeormService.postRelationship( + 1, + 'manager', + managerData as any + ); + + const checkUserAfterPost = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + if (!checkUserAfterPost) { + throw new Error('not found'); + } + + expect(checkUserAfterPost.manager.id.toString()).toBe(managerData.id); + expect(checkUserAfterPost.roles.map((i) => i.id.toString())).toEqual([ + ...checkUser.roles.map((i) => i.id.toString()), + ...rolesData.map((i) => i.id), + ]); + expect(checkUserAfterPost.userGroup.id.toString()).toBe(userGroupData.id); + + expect(result.data.map((i) => i.id)).toEqual( + checkUserAfterPost.roles.map((i) => i.id.toString()) + ); + expect(result2.data?.id).toEqual(checkUserAfterPost.manager.id.toString()); + expect(result1.data?.id).toEqual( + checkUserAfterPost.userGroup.id.toString() + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts new file mode 100644 index 00000000..695c9ff1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts @@ -0,0 +1,40 @@ +import { + EntityRelation, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; +import { PostRelationshipData } from '../../../mixin/zod'; +import { getRelationship } from '../get-relationship/get-relationship'; +import { TypeOrmService } from '../../service'; + +export async function postRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: TypeOrmService, + id: number | string, + rel: Rel, + input: PostRelationshipData +): Promise> { + const idsResult = await this.typeormUtilsService.validateRelationInputData( + rel, + input + ); + const postBuilder = this.repository + .createQueryBuilder() + .relation(rel.toString()) + .of(id); + + if (Array.isArray(idsResult)) { + await postBuilder.add(idsResult); + } else { + await postBuilder.set(idsResult); + } + + return getRelationship.call< + TypeOrmService, + [number | string, Rel], + Promise> + >(this, id, rel); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts new file mode 100644 index 00000000..981d2db4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts @@ -0,0 +1,85 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; + +import { + createAndPullSchemaBase, + mockDBTestModule, + providerEntities, + UserGroups, + Users, +} from '../../../mock-utils'; +import { CurrentDataSourceProvider } from '../factory'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; +import { EntityPropsMapService } from './entity-props-map.service'; + +describe('EntityPropsMapService', () => { + let db: IMemoryDb; + let entityPropsMapService: EntityPropsMapService; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + EntityPropsMapService, + ], + }).compile(); + + entityPropsMapService = module.get( + EntityPropsMapService + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('getPropsForEntity', () => { + const result = entityPropsMapService.getRelPropsForEntity(Users); + expect(result).toEqual([ + 'addresses', + 'manager', + 'roles', + 'comments', + 'notes', + 'userGroup', + ]); + }); + + it('getPropsForEntity', () => { + const result = entityPropsMapService.getPropsForEntity(Users); + expect(result).toEqual([ + 'id', + 'login', + 'firstName', + 'testReal', + 'testArrayNull', + 'lastName', + 'isActive', + 'createdAt', + 'testDate', + 'updatedAt', + ]); + }); + + it('getPrimaryColumnsForEntity', () => { + expect(entityPropsMapService.getPrimaryColumnsForEntity(Users)).toBe('id'); + }); + + it('getNameForEntity', () => { + expect(entityPropsMapService.getNameForEntity(Users)).toBe('Users'); + expect(entityPropsMapService.getNameForEntity(UserGroups)).toBe( + 'UserGroups' + ); + }); + + it('getRelationPropsType', () => { + expect( + entityPropsMapService.getRelationPropsType(Users, 'userGroup' as any) + ).toEqual(UserGroups); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts new file mode 100644 index 00000000..160f2028 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts @@ -0,0 +1,102 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, EntityTarget } from 'typeorm'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +import { CURRENT_DATA_SOURCE_TOKEN } from '../constants'; +import { ObjectLiteral as Entity } from '../../../types'; +import { + ResultGetField, + TupleOfEntityProps, + TupleOfEntityRelation, +} from '../../mixin/types'; +import { getField } from '../orm-helper'; + +@Injectable() +export class EntityPropsMapService { + @Inject(CURRENT_DATA_SOURCE_TOKEN) private dataSource!: DataSource; + + private _propsForEntity: Map = new Map(); + private _relPropsForEntity: Map = new Map(); + private _relTypePropsForEntity: Map = new Map(); + private _primaryColumnsForEntity: Map = new Map(); + private _nameForEntity: Map = new Map(); + + getPropsForEntity(entity: E): TupleOfEntityProps { + const result = this._propsForEntity.get(entity); + if (result) { + return result; + } + + const { field } = this.pullPropsAndRelFoEntity(entity); + + return field; + } + + getRelPropsForEntity(entity: E): TupleOfEntityRelation { + const result = this._relPropsForEntity.get(entity); + if (result) { + return result; + } + + const { relations } = this.pullPropsAndRelFoEntity(entity); + + return relations; + } + + getRelationPropsType(entity: E, rel: EntityRelation) { + const result = this._relTypePropsForEntity.get(entity); + if (result) { + return result[rel]; + } + + const repo = this.dataSource.getRepository( + entity as unknown as EntityTarget + ); + const relToType = repo.metadata.relations.reduce((acum, item) => { + acum[item.propertyName as keyof E] = item.inverseEntityMetadata.target; + return acum; + }, {} as Record); + + this._relTypePropsForEntity.set(entity, relToType); + return relToType[rel]; + } + + getPrimaryColumnsForEntity(entity: E): keyof E { + const result = this._primaryColumnsForEntity.get(entity); + if (result) { + return result as keyof E; + } + const primaryColumns = this.dataSource.getRepository( + entity as unknown as EntityTarget + ).metadata.primaryColumns[0].propertyName; + this._primaryColumnsForEntity.set(entity, primaryColumns); + + return primaryColumns as keyof E; + } + + getNameForEntity(entity: E): string { + const result = this._nameForEntity.get(entity); + if (result) { + return result; + } + const name = this.dataSource.getRepository( + entity as unknown as EntityTarget + ).metadata.name; + this._nameForEntity.set(entity, name); + return name; + } + + private pullPropsAndRelFoEntity( + entity: E + ): ResultGetField { + const repo = this.dataSource.getRepository( + entity as unknown as EntityTarget + ); + + const { relations, field } = getField(repo); + this._propsForEntity.set(entity, field); + this._relPropsForEntity.set(entity, relations); + + return { relations, field }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts new file mode 100644 index 00000000..2a3590c8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts @@ -0,0 +1,4 @@ +export * from './type-orm.service'; +export * from './transform-data.service'; +export * from './typeorm-utils.service'; +export * from './entity-props-map.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts new file mode 100644 index 00000000..fed6e89a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts @@ -0,0 +1,372 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../mock-utils'; +import { CurrentDataSourceProvider, CurrentEntityRepository } from '../factory'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; +import { TransformDataService } from './transform-data.service'; +import { ApplicationConfig } from '@nestjs/core'; +import { VersioningType } from '@nestjs/common'; +import { EntityPropsMapService } from '../service'; + +describe('TransformDataService', () => { + let db: IMemoryDb; + let transformDataService: TransformDataService; + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + let applicationConfig: ApplicationConfig; + let entityPropsMapService: EntityPropsMapService; + let usersData: Users; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + // TransformDataService, + // EntityPropsMapService, + ], + }).compile(); + + // ({ + // userRepository, + // addressesRepository, + // notesRepository, + // commentsRepository, + // rolesRepository, + // userGroupRepository, + // } = getRepository(module)); + // await pullAllData( + // userRepository, + // addressesRepository, + // notesRepository, + // commentsRepository, + // rolesRepository, + // userGroupRepository + // ); + + // transformDataService = + // module.get>(TransformDataService); + // entityPropsMapService = module.get( + // EntityPropsMapService + // ); + // + // const data = await userRepository.findOne({ + // where: { + // id: 1, + // }, + // relations: { + // userGroup: true, + // manager: true, + // roles: true, + // addresses: true, + // comments: true, + // notes: true, + // }, + // }); + // if (!data) { + // throw new Error('Not found user'); + // } + // usersData = data; + // applicationConfig = module.get(ApplicationConfig); + }); + + beforeEach(() => { + // transformDataService = new TransformDataService(); + // Object.defineProperty(transformDataService, 'applicationConfig', { + // value: applicationConfig, + // }); + // Object.defineProperty(transformDataService, 'entityPropsMapService', { + // value: entityPropsMapService, + // }); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Test', () => {}); + + // it('Url path', () => { + // const prefix = 'api'; + // applicationConfig.setGlobalPrefix(prefix); + // + // expect(transformDataService.urlPath).toEqual(['', prefix]); + // }); + // it('Url path with version', () => { + // const prefix = 'api'; + // const version = '1'; + // + // applicationConfig.setGlobalPrefix(prefix); + // + // jest + // .spyOn(applicationConfig, 'getVersioning') + // .mockImplementationOnce(() => ({ + // type: VersioningType.URI, + // defaultVersion: version, + // })); + // + // expect(transformDataService.urlPath).toEqual(['', prefix, 'v1']); + // }); + // + // it('check type, id and links', () => { + // const result = transformDataService.transformData(usersData); + // expect(result).toHaveProperty('data'); + // const { data } = result; + // expect(data.type).toBe('users'); + // expect(data.id).toBe(usersData.id.toString()); + // expect(data.links.self).toBe( + // [...transformDataService.urlPath, 'users', usersData.id.toString()].join( + // '/' + // ) + // ); + // }); + // + // it('check attributes', async () => { + // const result = transformDataService.transformData(usersData); + // const { + // id, + // addresses, + // comments, + // manager, + // roles, + // notes, + // userGroup, + // ...other + // } = usersData; + // expect(result.data.attributes).toEqual(other); + // const result1 = transformDataService.transformData([]); + // expect(result1.data).toEqual([]); + // + // const data = await userRepository.findOne({ + // select: { + // id: true, + // login: true, + // isActive: true, + // testDate: true, + // }, + // where: { + // id: 1, + // }, + // }); + // if (!data) { + // throw new Error('Not found user'); + // } + // const result2 = transformDataService.transformData(data); + // const { id: id2, ...other2 } = data; + // + // expect(result2.data.attributes).toEqual(other2); + // }); + // + // it('check relationships', async () => { + // const { + // data: { relationships }, + // } = transformDataService.transformData(usersData); + // expect(relationships?.addresses?.data).toEqual({ + // type: 'addresses', + // id: usersData.addresses.id.toString(), + // }); + // expect(relationships?.addresses?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/addresses` + // ); + // expect(relationships?.manager?.data).toEqual({ + // type: 'users', + // id: usersData.manager.id.toString(), + // }); + // expect(relationships?.manager?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/manager` + // ); + // expect(relationships?.roles?.data).toEqual( + // usersData.roles.map((i) => ({ + // type: 'roles', + // id: i.id.toString(), + // })) + // ); + // expect(relationships?.roles?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/roles` + // ); + // expect(relationships?.comments?.data).toEqual( + // usersData.comments.map((i) => ({ + // type: 'comments', + // id: i.id.toString(), + // })) + // ); + // expect(relationships?.comments?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/comments` + // ); + // expect(relationships?.notes?.data).toEqual( + // usersData.notes.map((i) => ({ + // type: 'notes', + // id: i.id.toString(), + // })) + // ); + // expect(relationships?.notes?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/notes` + // ); + // expect(relationships?.userGroup?.data).toEqual({ + // type: 'user-groups', + // id: usersData.userGroup.id.toString(), + // }); + // expect(relationships?.userGroup?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/userGroup` + // ); + // }); + // + // it('check relationships again', async () => { + // const data = await userRepository.findOne({ + // where: { + // id: 1, + // }, + // relations: { + // userGroup: true, + // roles: true, + // }, + // }); + // if (!data) { + // throw new Error('Not found user'); + // } + // + // const { + // data: { relationships }, + // } = transformDataService.transformData(data); + // + // expect(relationships?.addresses).not.toHaveProperty('data'); + // expect(relationships?.addresses?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/addresses` + // ); + // expect(relationships?.manager).not.toHaveProperty('data'); + // expect(relationships?.manager?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/manager` + // ); + // expect(relationships?.roles?.data).toEqual( + // usersData.roles.map((i) => ({ + // type: 'roles', + // id: i.id.toString(), + // })) + // ); + // expect(relationships?.roles?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/roles` + // ); + // expect(relationships?.comments).not.toHaveProperty('data'); + // expect(relationships?.comments?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/comments` + // ); + // expect(relationships?.notes).not.toHaveProperty('data'); + // expect(relationships?.notes?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/notes` + // ); + // expect(relationships?.userGroup?.data).toEqual({ + // type: 'user-groups', + // id: usersData.userGroup.id.toString(), + // }); + // expect(relationships?.userGroup?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/userGroup` + // ); + // data.userGroup = null as any; + // data.roles = []; + // const { + // data: { relationships: relationships2 }, + // } = transformDataService.transformData(data); + // + // expect(relationships2?.addresses).not.toHaveProperty('data'); + // expect(relationships2?.addresses?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/addresses` + // ); + // expect(relationships2?.manager).not.toHaveProperty('data'); + // expect(relationships2?.manager?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/manager` + // ); + // + // expect(relationships2?.roles?.data).toEqual([]); + // expect(relationships2?.roles?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/roles` + // ); + // expect(relationships2?.comments).not.toHaveProperty('data'); + // expect(relationships2?.comments?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/comments` + // ); + // expect(relationships2?.notes).not.toHaveProperty('data'); + // expect(relationships2?.notes?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/notes` + // ); + // expect(relationships2?.userGroup?.data).toEqual(null); + // expect(relationships2?.userGroup?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/userGroup` + // ); + // }); + // + // it('check include', async () => { + // const data = await userRepository.findOne({ + // where: { + // id: 1, + // }, + // relations: { + // userGroup: true, + // roles: true, + // }, + // }); + // if (!data) { + // throw new Error('Not found user'); + // } + // + // const { included } = transformDataService.transformData(data); + // expect(included).not.toBe(undefined); + // }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts new file mode 100644 index 00000000..b6ac2c93 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts @@ -0,0 +1,259 @@ +import { Inject, Injectable, VersioningType } from '@nestjs/common'; +import { ApplicationConfig } from '@nestjs/core'; +import { + Attributes, + Data, + MainData, + Relationships, + ResourceData, + ResourceObject, + camelToKebab, + ObjectTyped, + EntityRelation, +} from '@klerick/json-api-nestjs-shared'; + +import { RoutePathFactory } from '@nestjs/core/router/route-path-factory'; +import { EntityPropsMapService } from './entity-props-map.service'; +import { ObjectLiteral } from '../../../types'; + +Injectable(); +export class TransformDataService { + @Inject(ApplicationConfig) private applicationConfig!: ApplicationConfig; + @Inject(EntityPropsMapService) + private entityPropsMapService!: EntityPropsMapService; + + private _urlPath!: string[]; + + get urlPath() { + if (this._urlPath) return [...this._urlPath]; + this._urlPath = ['']; + const prefix = this.applicationConfig.getGlobalPrefix(); + const version = this.applicationConfig.getVersioning(); + + const routePathFactory = new RoutePathFactory(this.applicationConfig); + + if (prefix) { + this._urlPath.push(this.applicationConfig.getGlobalPrefix()); + } + if (version && version.type === VersioningType.URI) { + const firstVersion = Array.isArray(version.defaultVersion) + ? version.defaultVersion[0] + : version.defaultVersion; + if (firstVersion) { + this._urlPath.push( + `${routePathFactory.getVersionPrefix( + version + )}${firstVersion.toString()}` + ); + } + } + return [...this._urlPath]; + } + private getLink(...partOfUrl: string[]) { + const urlPath = this.urlPath; + urlPath.push(...partOfUrl); + return urlPath.join('/'); + } + + public transformData(data: E): Pick, 'data' | 'included'>; + public transformData( + data: E[] + ): Pick, 'data' | 'included'>; + public transformData( + data: E | E[] + ): + | Pick, 'data' | 'included'> + | Pick, 'data' | 'included'> { + const dataResponse = Array.isArray(data) + ? data + .map((i) => this.getMainData(i)) + .filter((i: ResourceData | null): i is ResourceData => !!i) + : this.getMainData(data); + + let relationships: Relationships | undefined = undefined; + if (Array.isArray(dataResponse) && dataResponse.length > 0) { + relationships = dataResponse.reduce( + (acum, item) => ({ + ...acum, + ...item.relationships, + }), + {} as Relationships + ); + } + + if (!Array.isArray(dataResponse) && dataResponse !== null) { + relationships = dataResponse.relationships; + } + + if (relationships === undefined) { + return { data: dataResponse } as Pick< + ResourceObject, + 'data' | 'included' + >; + } + + const propsNeedForInclude = ObjectTyped.entries(relationships).reduce( + (acum, [key, val]) => { + if (!val || !('data' in val)) { + return acum; + } + if (Array.isArray(val.data)) { + acum.push(key); + return acum; + } + if (!Array.isArray(val.data)) { + acum.push(key); + } + return acum; + }, + [] as EntityRelation[] + ); + + const included = (Array.isArray(data) ? data : [data]) + .reduce((acum, item) => { + const tmp = propsNeedForInclude.reduce((a, props) => { + const currentItem = item[props]; + type CurrentItem = typeof currentItem; + const r = ( + Array.isArray(currentItem) ? currentItem : [currentItem] + ) as CurrentItem[]; + + a.push(...r); + return a; + }, [] as E[EntityRelation][]); + acum.push(...tmp); + return acum; + }, [] as E[EntityRelation][]) + .map((i) => { + return this.getMainData(i as E); + }) + .filter((i) => !!i); + + if (included.length > 0) { + return { data: dataResponse, included } as unknown as Pick< + ResourceObject, + 'data' | 'included' + >; + } + return { data: dataResponse } as unknown as Pick< + ResourceObject, + 'data' | 'included' + >; + } + + getMainData(data: E): ResourceData | null { + if (!data) return null; + const entity = data.constructor as unknown as E; + return { + ...this.getData( + camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), + data[this.entityPropsMapService.getPrimaryColumnsForEntity(entity)] as + | string + | number + ), + attributes: ObjectTyped.entries(data).reduce((acum, [key, val]) => { + if ( + this.entityPropsMapService + .getRelPropsForEntity(entity) + .includes(key.toString()) || + key.toString() === + this.entityPropsMapService.getPrimaryColumnsForEntity(entity) + ) { + return acum; + } + acum[key] = val; + return acum; + }, {} as Record) as Attributes, + relationships: this.transformRelationshipsData(data), + links: { + self: this.getLink( + camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), + data[ + this.entityPropsMapService.getPrimaryColumnsForEntity(entity) + ] as string + ), + }, + }; + } + + getRelationships>( + data: E, + rel: Rel + ): Data { + const entity = data.constructor as unknown as E; + const relation = data[rel]; + const target = this.entityPropsMapService.getRelationPropsType(entity, rel); + const typeName = camelToKebab( + this.entityPropsMapService.getNameForEntity(target) + ) as Rel; + + const primaryColumn = + this.entityPropsMapService.getPrimaryColumnsForEntity(target); + + const resultData = Array.isArray(relation) + ? (relation as E[Rel][]).map((i: E[Rel]) => + this.getData( + typeName, + i[primaryColumn as keyof E[Rel]] as string + ) + ) + : relation === null || !relation + ? null + : this.getData( + typeName, + relation[primaryColumn as keyof E[Rel]] as string + ); + + return { + data: resultData, + } as Data; + } + + transformRelationshipsData(data: E): Relationships { + const entity = data.constructor as unknown as E; + return this.entityPropsMapService + .getRelPropsForEntity(entity) + .reduce((acum: any, val: any) => { + const result: { + links: Record; + data?: Data], EntityRelation>['data']; + } = { + links: { + self: this.getLink( + camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), + data[ + this.entityPropsMapService.getPrimaryColumnsForEntity( + entity + ) as keyof E + ] as string, + 'relationships', + val + ), + }, + }; + + if (val in data) { + const { data: dataRelationships } = this.getRelationships( + data, + val as EntityRelation + ); + if (Array.isArray(dataRelationships)) { + result['data'] = dataRelationships; + } + + if (!Array.isArray(dataRelationships)) { + result['data'] = dataRelationships; + } + } + acum[val.toString()] = result; + return acum; + }, {} as Record) as Relationships; + } + + getData(type: T, id: number | string): MainData { + return { + type, + id: id.toString(), + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts new file mode 100644 index 00000000..2e80c4f2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts @@ -0,0 +1,135 @@ +import { + ResourceObject, + EntityRelation, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; +import { Inject } from '@nestjs/common'; +import { Repository } from 'typeorm'; + +import { OrmService } from '../../mixin/types'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../../mixin/zod'; +import { ConfigParam, ObjectLiteral } from '../../../types'; + +import { + getAll, + getOne, + deleteOne, + postOne, + patchOne, + getRelationship, + postRelationship, + deleteRelationship, + patchRelationship, +} from '../orm-methods'; + +import { TypeormUtilsService } from './typeorm-utils.service'; +import { TransformDataService } from './transform-data.service'; +import { CONTROL_OPTIONS_TOKEN } from '../../../constants'; +import { CURRENT_ENTITY_REPOSITORY } from '../constants'; +import { TypeOrmParam } from '../type'; + +export class TypeOrmService implements OrmService { + @Inject(TypeormUtilsService) + public typeormUtilsService!: TypeormUtilsService; + @Inject(TransformDataService) + public transformDataService!: TransformDataService; + @Inject(CONTROL_OPTIONS_TOKEN) public config!: ConfigParam & TypeOrmParam; + @Inject(CURRENT_ENTITY_REPOSITORY) public repository!: Repository; + + getAll(query: Query): Promise> { + return getAll.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, query); + } + + deleteOne(id: number | string): Promise { + return deleteOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id); + } + + deleteRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise { + return deleteRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } + + getOne(id: number | string, query: QueryOne): Promise> { + return getOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, query); + } + + getRelationship>( + id: number | string, + rel: Rel + ): Promise> { + return getRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel); + } + + patchOne( + id: number | string, + inputData: PatchData + ): Promise> { + return patchOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, inputData); + } + + patchRelationship>( + id: number | string, + rel: Rel, + input: PatchRelationshipData + ): Promise> { + return patchRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } + + postOne(inputData: PostData): Promise> { + return postOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, inputData); + } + + postRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise> { + return postRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts new file mode 100644 index 00000000..9c32607f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts @@ -0,0 +1,770 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { + createAndPullSchemaBase, + mockDBTestModule, + providerEntities, + UserGroups, + Users, + Comments, + Roles, + Addresses, + Notes, + getRepository, + pullAllData, +} from '../../../mock-utils'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, +} from '../factory'; +import { CURRENT_ENTITY_REPOSITORY } from '../constants'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; +import { TypeormUtilsService } from './typeorm-utils.service'; +import { PostData, PostRelationshipData, Query } from '../../mixin/zod'; +import { QueryField, FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { + EXPRESSION, + OperandsMapExpression, + ObjectLiteral as Entity, +} from '../../../types'; +import { + BadRequestException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; + +function getDefaultQuery() { + const filter = { + relation: null, + target: null, + }; + const defaultQuery: Query = { + [QueryField.filter]: filter, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: 1, + number: 1, + }, + }; + + return defaultQuery; +} + +describe('TypeormUtilsService', () => { + let db: IMemoryDb; + let typeormUtilsServiceUserGroups: TypeormUtilsService; + let repositoryUserGroups: Repository; + + let typeormUtilsServiceUser: TypeormUtilsService; + let repositoryUser: Repository; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + function getQuery() { + return repositoryUser + .createQueryBuilder() + .subQuery() + .select('Users-Roles.user_id') + .from('users_have_roles', 'Users-Roles') + .leftJoin( + Roles, + 'Users__Roles_roles', + 'Users-Roles.role_id = Users__Roles_roles.id' + ); + } + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + CurrentEntityManager(), + CurrentEntityRepository(UserGroups), + TypeormUtilsService, + ], + }).compile(); + + typeormUtilsServiceUserGroups = + module.get>(TypeormUtilsService); + repositoryUserGroups = module.get>( + CURRENT_ENTITY_REPOSITORY + ); + + const moduleUsers: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + ], + }).compile(); + + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + + typeormUtilsServiceUser = + moduleUsers.get>(TypeormUtilsService); + repositoryUser = moduleUsers.get>( + CURRENT_ENTITY_REPOSITORY + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('TypeormUtilsService.currentAlias', () => { + expect(typeormUtilsServiceUserGroups.currentAlias).toBe('UserGroups'); + }); + + it('TypeormUtilsService.getAliasForRelation', () => { + expect(typeormUtilsServiceUserGroups.getAliasForRelation('users')).toBe( + 'UserGroups__Users_users' + ); + }); + + it('TypeormUtilsService.getAliasPath', () => { + expect(typeormUtilsServiceUserGroups.getAliasPath('id')).toBe( + 'UserGroups.id' + ); + expect( + typeormUtilsServiceUserGroups.getAliasPath('Users', 'UserGroups') + ).toBe('UserGroups.Users'); + expect( + typeormUtilsServiceUserGroups.getAliasPath('Users', 'UserGroups', '-') + ).toBe('UserGroups-Users'); + expect( + typeormUtilsServiceUserGroups.getAliasPath('label', 'users', '-') + ).toBe('Users-label'); + }); + + describe('asyncIterateFindRelationships', () => { + it('should be ok', async () => { + const notes = await notesRepository.find(); + const userGroup = await userGroupRepository.find(); + + const data: PostData['relationships'] = { + notes: { + data: [ + { + type: 'notes', + id: notes[0].id, + }, + ], + }, + manager: { + data: { + type: 'users', + id: '1', + }, + }, + userGroup: { + data: { + type: 'users-group', + id: `${userGroup[0].id}`, + }, + }, + }; + + const result = []; + for await (const item of typeormUtilsServiceUser.asyncIterateFindRelationships( + data + )) { + result.push(item); + } + + expect(result[0]).toHaveProperty('notes'); + expect(result[0]['notes']).toEqual([{ id: notes[0].id }]); + + expect(result[1]).toHaveProperty('manager'); + expect(result[1]['manager']).toEqual({ id: 1 }); + + expect(result[2]).toHaveProperty('userGroup'); + expect(result[2]['userGroup']).toEqual({ id: userGroup[0].id }); + }); + + it('should be error props incorrect', async () => { + const data = { + incorrectProps: { + type: 'users', + id: '1', + }, + } as any; + expect.assertions(1); + try { + await typeormUtilsServiceUser + .asyncIterateFindRelationships(data) + .next(); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + it('should be error resource not found', async () => { + const data: PostData['relationships'] = { + manager: { + data: { + id: '1000', + type: 'users', + }, + }, + }; + expect.assertions(1); + try { + await typeormUtilsServiceUser + .asyncIterateFindRelationships(data) + .next(); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + }); + + describe('getFilterExpressionForTarget', () => { + it('expression for target field with null', () => { + const nullableField = 'id'; + const notNullableField = 'login'; + const query = getDefaultQuery(); + query.filter.target = { + [nullableField]: { + [FilterOperand.eq]: null, + }, + [notNullableField]: { + [FilterOperand.ne]: null, + }, + }; + + function guardField( + filter: any, + field: any + ): asserts field is keyof R { + if (filter && !(field in filter)) + throw new Error('field not exist in query filter'); + } + + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + const mainAliasCheck = 'Users'; + + for (const item of result) { + const { params, alias, expression, selectInclude } = item; + expect(selectInclude).toBe(undefined); + if (!alias) { + expect(alias).not.toBe(undefined); + throw new Error('alias in undefined for result'); + } + const [mainAlias, field] = alias.split('.'); + expect(mainAlias).toBe(mainAliasCheck); + guardField(query.filter.target, field); + const filterName: any = query.filter.target[field]; + if (!filterName) { + expect(filterName).not.toBe(undefined); + throw new Error('filterName in undefined from query'); + } + + expect(params).toBe(undefined); + + if (field === nullableField) { + expect(expression).toBe('IS NULL'); + continue; + } + + if (field === notNullableField) { + expect(expression).toBe('IS NOT NULL'); + continue; + } + + throw new Error('filed is incorrect'); + } + }); + it('expression for target field', () => { + const valueTest = (filterOperand: FilterOperand) => + `test for ${filterOperand}`; + const valueTestArray = ( + filterOperand: FilterOperand.nin | FilterOperand.in + ): [string, ...string[]] => [valueTest(filterOperand)]; + + const query = getDefaultQuery(); + query.filter.target = { + id: { + [FilterOperand.eq]: valueTest(FilterOperand.eq), + [FilterOperand.ne]: valueTest(FilterOperand.ne), + }, + isActive: { + [FilterOperand.like]: valueTest(FilterOperand.like), + [FilterOperand.regexp]: valueTest(FilterOperand.regexp), + }, + firstName: { + [FilterOperand.gt]: valueTest(FilterOperand.gt), + [FilterOperand.gte]: valueTest(FilterOperand.gte), + }, + testDate: { + [FilterOperand.lt]: valueTest(FilterOperand.lt), + [FilterOperand.lte]: valueTest(FilterOperand.lt), + }, + createdAt: { + [FilterOperand.in]: valueTestArray(FilterOperand.in), + [FilterOperand.nin]: valueTestArray(FilterOperand.nin), + }, + }; + + function guardField( + filter: any, + field: any + ): asserts field is keyof R { + if (filter && !(field in filter)) + throw new Error('field not exist in query filter'); + } + + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + const mainAliasCheck = 'Users'; + const paramsNameSet = new Set(); + for (const item of result) { + const { params, alias, expression, selectInclude } = item; + expect(selectInclude).toBe(undefined); + if (!alias) { + expect(alias).not.toBe(undefined); + throw new Error('alias in undefined for result'); + } + const [mainAlias, field] = alias.split('.'); + expect(mainAlias).toBe(mainAliasCheck); + guardField(query.filter.target, field); + const filterName: any = query.filter.target[field]; + if (!filterName) { + expect(filterName).not.toBe(undefined); + throw new Error('filterName in undefined from query'); + } + if (!params) { + expect(params).not.toBe(undefined); + throw new Error('params in undefined for result'); + } + if (Array.isArray(params)) { + expect(params).not.toBeInstanceOf(Array); + throw new Error('params in undefined for result'); + } + const { val, name } = params; + expect(paramsNameSet.has(name)).toBe(false); + paramsNameSet.add(name); + const reg = new RegExp(`params_${alias}_\\d{1,}`); + const regResult = name.match(reg); + + if (regResult === null) { + expect(name.match(reg)).not.toBe(null); + throw new Error(`name is not pattern: params_${alias}_\\d{1,}`); + } + const expressionMap = expression.replace(name, EXPRESSION); + const checkFilterOperand = Object.entries(FilterOperand).find( + ([key, val]) => OperandsMapExpression[val] === expressionMap + ); + if (!checkFilterOperand) { + expect(checkFilterOperand).not.toBe(undefined); + throw new Error(`expression incorrect`); + } + + const operand = checkFilterOperand[0] as any; + guardField(filterName, operand); + if (operand === 'like') { + expect(params.val).toEqual(`%${filterName[operand]}%`); + } else { + expect(params.val).toEqual(filterName[operand]); + } + } + }); + it('expression for target relation field with relation column', () => { + const query = getDefaultQuery(); + query.filter.target = { + addresses: { + [FilterOperand.eq]: 'null', + [FilterOperand.ne]: 'null', + }, + }; + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + expect(result.length).toBe(2); + const [first, second] = result; + expect(first).not.toHaveProperty('params'); + expect(first).not.toHaveProperty('selectInclude'); + expect(first['alias']).toBe('Users.addresses'); + expect(first['expression']).toBe('IS NULL'); + expect(second).not.toHaveProperty('params'); + expect(second).not.toHaveProperty('selectInclude'); + expect(second['alias']).toBe('Users.addresses'); + expect(second['expression']).toBe('IS NOT NULL'); + }); + it('expression for target relation field with one-to-many', () => { + const query = getDefaultQuery(); + query.filter.target = { + comments: { + [FilterOperand.eq]: 'null', + [FilterOperand.ne]: 'null', + }, + }; + const subQuery = repositoryUser + .createQueryBuilder() + .subQuery() + .select('Comments.createdBy', 'createdBy') + .from(Comments, 'Comments') + .where(`Comments.createdBy = Users.id`) + .getQuery(); + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + expect(result.length).toBe(2); + const [first, second] = result; + expect(first).not.toHaveProperty('params'); + expect(first).not.toHaveProperty('selectInclude'); + expect(first).not.toHaveProperty('alias'); + expect(first['expression']).toBe(`NOT EXISTS ${subQuery}`); + expect(second).not.toHaveProperty('params'); + expect(second).not.toHaveProperty('selectInclude'); + expect(second).not.toHaveProperty('alias'); + expect(second['expression']).toBe(`EXISTS ${subQuery}`); + }); + it('expression for target relation field with many-to-many', () => { + const query = getDefaultQuery(); + query.filter.target = { + roles: { + [FilterOperand.eq]: 'null', + [FilterOperand.ne]: 'null', + }, + }; + const subQuery = getQuery() + .where(`Users-Roles.user_id = Users.id`) + .getQuery(); + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + + expect(result.length).toBe(2); + const [first, second] = result; + expect(first).not.toHaveProperty('params'); + expect(first).not.toHaveProperty('selectInclude'); + expect(first).not.toHaveProperty('alias'); + expect(first['expression']).toBe(`NOT EXISTS ${subQuery}`); + expect(second).not.toHaveProperty('params'); + expect(second).not.toHaveProperty('selectInclude'); + expect(second).not.toHaveProperty('alias'); + expect(second['expression']).toBe(`EXISTS ${subQuery}`); + }); + }); + + describe('getFilterExpressionForRelation', () => { + it('expression for relation many-to-many', () => { + const query = getDefaultQuery(); + const conditional = { + name: { + [FilterOperand.eq]: 'null', + [FilterOperand.ne]: 'null', + }, + createdAt: { + [FilterOperand.eq]: 'test1', + [FilterOperand.ne]: 'test2', + [FilterOperand.nin]: ['test3'] as [string, ...string[]], + }, + }; + + query.filter.relation = { + roles: conditional, + }; + + let subQuery = getQuery() + .where(`"Users__Roles_roles"."name" IS NULL`) + .andWhere(`"Users__Roles_roles"."name" IS NOT NULL`) + .andWhere(`"Users__Roles_roles"."created_at" = :param1`) + .andWhere(`"Users__Roles_roles"."created_at" <> :param2`) + .andWhere(`"Users__Roles_roles"."created_at" NOT IN (:...param3)`) + .getQuery(); + + const result = + typeormUtilsServiceUser.getFilterExpressionForRelation(query); + + expect(result.length).toBe(1); + + const [first] = result; + expect(first).not.toHaveProperty('selectInclude'); + if (!first.params && !Array.isArray(first.params)) { + expect(first).toHaveProperty('params'); + expect(first.params).toBeInstanceOf(Array); + } + if (Array.isArray(first.params)) { + expect(first?.params?.length).toBe(3); + const [firstParams, secondParams, thirdParams] = first.params; + expect(firstParams?.val).toBe( + query.filter?.relation?.roles?.createdAt?.eq + ); + + const regResult1 = firstParams?.name.match( + new RegExp(`params_Roles.createdAt_\\d{1,}`) + ); + if (regResult1) { + subQuery = subQuery.replace('param1', regResult1[0]); + } + expect(regResult1).not.toBe(null); + + expect(secondParams?.val).toBe( + query.filter?.relation?.roles?.createdAt?.ne + ); + + const regResult2 = secondParams?.name.match( + new RegExp(`params_Roles.createdAt_\\d{1,}`) + ); + if (regResult2) { + subQuery = subQuery.replace('param2', regResult2[0]); + } + expect(regResult2).not.toBe(null); + + expect(thirdParams?.val).toBe( + query.filter?.relation?.roles?.createdAt?.nin + ); + const regResult3 = thirdParams?.name.match( + new RegExp(`params_Roles.createdAt_\\d{1,}`) + ); + if (regResult3) { + subQuery = subQuery.replace('param3', regResult3[0]); + } + } + expect(first.alias).toBe(`Users.id`); + expect(first.expression).toBe(`IN ${subQuery}`); + }); + + it('expression for relation other type', () => { + const query = getDefaultQuery(); + query.filter.relation = { + addresses: { + createdAt: { + eq: 'qweqwe', + }, + }, + comments: { + createdAt: { + like: 'sdfsdf', + }, + }, + }; + const firstAlias = 'Addresses.createdAt'; + const secondAlias = 'Comments.createdAt'; + const result = + typeormUtilsServiceUser.getFilterExpressionForRelation(query); + + expect(result.length).toBe(2); + const [first, second] = result; + + const firstResult = first.expression.match( + new RegExp(`params_${firstAlias}_\\d{1,}`) + ); + + if (!firstResult) { + expect(firstResult).not.toBe(null); + throw Error('Should be like pattern'); + } + + expect(first.expression).toBe(`= :${firstResult[0]}`); + expect(first.alias).toBe(`Users__Addresses_addresses.createdAt`); + expect(first.selectInclude).toBe('addresses'); + if (!Array.isArray(first.params)) { + expect(first.params?.name).toBe(`${firstResult[0]}`); + expect(first.params?.val).toBe( + query.filter.relation?.addresses?.createdAt?.eq + ); + } else { + expect(first.params).not.toBeInstanceOf(Array); + } + + const secondResult = second.expression.match( + new RegExp(`params_${secondAlias}_\\d{1,}`) + ); + if (!secondResult) { + expect(secondResult).not.toBe(null); + throw Error('Should be like pattern'); + } + + expect(second.expression).toBe(`ILIKE :${secondResult[0]}`); + expect(second.alias).toBe('Users__Comments_comments.createdAt'); + expect(second.selectInclude).toBe('comments'); + if (!Array.isArray(second.params)) { + expect(second.params?.name).toBe(secondResult[0]); + expect(second.params?.val).toBe( + `%${query.filter.relation?.comments?.createdAt?.like}%` + ); + } else { + expect(second.params).not.toBeInstanceOf(Array); + } + }); + }); + + describe('validateRelationInputData', () => { + let usersData: Users; + beforeEach(async () => { + const result = await userRepository.findOne({ + where: { + id: 1, + }, + relations: { + roles: true, + userGroup: true, + manager: true, + }, + }); + if (!result) { + throw Error('not found mock data'); + } + usersData = result; + }); + it('should be ok', async () => { + const rolesData = usersData.roles.map((i) => ({ + type: 'roles', + id: i.id.toString(), + })); + + const userGroupData = { + type: 'user-groups', + id: usersData.userGroup.id.toString(), + }; + const managerData = { + type: 'users', + id: usersData.manager.id.toString(), + }; + const emptyRoles: { id: string; type: string }[] = []; + const emptyManager = null; + const result = await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + rolesData + ); + const result1 = await typeormUtilsServiceUser.validateRelationInputData( + 'userGroup', + userGroupData + ); + const result2 = await typeormUtilsServiceUser.validateRelationInputData( + 'manager', + managerData + ); + const result3 = await typeormUtilsServiceUser.validateRelationInputData( + 'manager', + emptyManager + ); + const result4 = await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + emptyRoles + ); + expect(result).toEqual(usersData.roles.map((i) => i.id.toString())); + expect(result1).toEqual(usersData.userGroup.id.toString()); + expect(result2).toEqual(usersData.manager.id.toString()); + expect(result3).toEqual(emptyManager); + expect(result4).toEqual(emptyRoles); + }); + + it('Should be error incorrect type name', async () => { + const rolesData = usersData.roles.map((i, index) => ({ + type: index === 1 ? 'other' : 'roles', + id: i.id.toString(), + })) as PostRelationshipData; + + const userGroupData = { + type: 'userGroups', + id: usersData.userGroup.id.toString(), + }; + const managerData = { + type: 'user', + id: usersData.manager.id.toString(), + }; + expect.assertions(3); + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + rolesData + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'userGroup', + userGroupData + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'manager', + managerData + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + }); + + it('Should be error, Incorrect relation type', async () => { + expect.assertions(2); + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + {} as any + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'userGroup', + [] as any + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + }); + + it('Should be error, Not fond', async () => { + const rolesData = usersData.roles.map((i, index) => ({ + type: 'roles', + id: index === 1 ? '1000' : i.id.toString(), + })) as PostRelationshipData; + expect.assertions(2); + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + rolesData + ); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + try { + await typeormUtilsServiceUser.validateRelationInputData('userGroup', { + type: 'user-groups', + id: '10000', + }); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts new file mode 100644 index 00000000..9683bd32 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts @@ -0,0 +1,685 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { EntityMetadata, Equal, In, Repository } from 'typeorm'; +import { RelationMetadata as TypeOrmRelationMetadata } from 'typeorm/metadata/RelationMetadata'; +import { + camelToKebab, + kebabToCamel, + ObjectTyped, + snakeToCamel, + FilterOperand, +} from '@klerick/json-api-nestjs-shared'; + +import { + ObjectLiteral, + EXPRESSION, + OperandMapExpressionForNull, + OperandsMapExpression, + OperandsMapExpressionForNullRelation, + ValidateQueryError, +} from '../../../types'; + +import { PatchData, PostData, Query } from '../../mixin/zod'; +import { CURRENT_ENTITY_REPOSITORY } from '../constants'; +import { TupleOfEntityRelation, EntityRelation } from '../../mixin/types'; +import { getEntityName } from '../../mixin/helper'; + +type RelationAlias = { + [K in TupleOfEntityRelation[number]]: string; +}; +type RelationMetadata = { + [K in TupleOfEntityRelation[number]]: TypeOrmRelationMetadata; +}; + +type ResultQueryExpressionObject = { name: string; val: string }; +type ResultQueryExpressionArray = { name: string; val: string }[]; + +export type RelationshipsResult = { + [K in EntityRelation]: E[K] extends E[K][] ? E[K] : E[K] | null; +}; + +export type ResultQueryExpression = { + alias?: string; + expression: string; + paramsForResult?: string[]; + params?: ResultQueryExpressionObject | ResultQueryExpressionArray; + selectInclude?: string; +}; +export type InputValidateData = { + type: string; + id: string; +}; + +export type ValidateReturn = T extends unknown[] + ? string[] + : T extends null + ? null + : string; + +type Entity = ObjectLiteral; + +function isTargetField( + relationField: TupleOfEntityRelation, + field: any +): field is TupleOfEntityRelation[number] { + return relationField.includes(field); +} + +function isRelationField( + relationField: TupleOfEntityRelation, + field: any +): asserts field is EntityRelation { + if (isTargetField(relationField, field)) return; + const error: ValidateQueryError = { + code: 'unrecognized_keys', + path: ['data', 'relationships'], + message: `Resource for relation '${field.toString()}' does not exist`, + keys: [field], + }; + + throw new BadRequestException([error]); +} + +Injectable(); +export class TypeormUtilsService { + private readonly _currentAlias!: string; + private readonly _relationMetadata = {} as RelationMetadata; + private readonly _relationAlias = {} as RelationAlias; + private readonly _relationFields!: TupleOfEntityRelation; + private readonly _entityMetadata!: EntityMetadata; + private _number = 0; + + constructor( + @Inject(CURRENT_ENTITY_REPOSITORY) private repository: Repository + ) { + this._currentAlias = snakeToCamel(repository.metadata.name); + const relationFields = []; + for (const metadata of repository.metadata.relations) { + const propertyName = + metadata.propertyName as TupleOfEntityRelation[number]; + this._relationMetadata[propertyName] = metadata; + this._relationAlias[propertyName] = snakeToCamel( + metadata.inverseEntityMetadata.name + ); + relationFields.push(propertyName); + } + this._relationFields = relationFields as TupleOfEntityRelation; + this._entityMetadata = repository.metadata; + } + get currentAlias() { + return this._currentAlias; + } + + get relationFields() { + return this._relationFields; + } + + relationName(relName: TupleOfEntityRelation[number]) { + return this._relationAlias[relName]; + } + + get currentPrimaryColumn(): keyof E { + return this._entityMetadata.primaryColumns[0].propertyName as keyof E; + } + + getAliasForRelation(relName: TupleOfEntityRelation[number]) { + return `${this.currentAlias}__${this._relationAlias[relName]}_${relName}`; + } + + getRelMetaDataForRelation(relName: TupleOfEntityRelation[number]) { + return this._relationMetadata[relName]; + } + + getPrimaryColumnForRel(relName: TupleOfEntityRelation[number]) { + return this._relationMetadata[relName].inverseEntityMetadata + .primaryColumns[0].propertyName; + } + + private getFilterObject(query: Query, filterType: 'target' | 'relation') { + const { filter } = query; + if (!filter) return null; + return filter[filterType]; + } + + private get number() { + if (this._number > 1000) { + this._number = 0; + } + this._number++; + return this._number; + } + + private getParamName(fieldName: string) { + return `params_${fieldName}_${this.number}`; + } + + getAliasPath(fieldName: unknown): string; + getAliasPath( + fieldName: unknown, + relname: TupleOfEntityRelation[number], + separator?: string + ): string; + getAliasPath(fieldName: unknown, relname: string, separator?: string): string; + getAliasPath( + fieldName: unknown, + relname?: TupleOfEntityRelation[number] | string, + separator = '.' + ): string { + const alias = relname + ? this._relationAlias[relname] || relname + : this.currentAlias; + return `${alias}${separator}${fieldName}`; + } + + private getSubQueryForManyToMany( + fieldName: TupleOfEntityRelation[number], + expression?: string[] + ): string { + const metadataRelation: TypeOrmRelationMetadata = + this._relationMetadata[fieldName]; + const relationPrimaryColumn = + metadataRelation.inverseEntityMetadata.primaryColumns[0].propertyName; + const { joinTableName, inverseJoinColumns, joinColumns } = + metadataRelation.isManyToManyOwner + ? metadataRelation + : metadataRelation.inverseRelation || metadataRelation; + + const { databaseName: queryJoinPropsName } = + metadataRelation.isManyToManyOwner + ? inverseJoinColumns[0] + : joinColumns[0]; + const { databaseName: selectJoinPropsName } = + metadataRelation.isManyToManyOwner + ? joinColumns[0] + : inverseJoinColumns[0]; + + const alias = this.getAliasPath( + this._relationAlias[fieldName], + this.currentAlias, + '-' + ); + + const selectAlias = this.getAliasPath(selectJoinPropsName, alias); + + const query = this.repository + .createQueryBuilder() + .subQuery() + .select(selectAlias) + .from(joinTableName, alias) + .leftJoin( + this._relationMetadata[fieldName].inverseEntityMetadata.target, + this.getAliasForRelation(fieldName), + `${this.getAliasPath(queryJoinPropsName, alias)} = ${this.getAliasPath( + relationPrimaryColumn, + this.getAliasForRelation(fieldName) + )}` + ); + if (!expression) { + query.where( + `${selectAlias} = ${this.getAliasPath(this.currentPrimaryColumn)}` + ); + } else { + for (const i in expression) { + query[i === '0' ? 'where' : 'andWhere'](expression[i]); + } + } + return query.getQuery(); + } + + getFilterExpressionForTarget(query: Query): ResultQueryExpression[] { + const resultExpression: ResultQueryExpression[] = []; + const filterTarget = this.getFilterObject(query, 'target'); + if (!filterTarget) return resultExpression; + for (const [fieldName, filter] of ObjectTyped.entries(filterTarget)) { + if (!filter) continue; + for (const entries of ObjectTyped.entries(filter)) { + const [operand, value] = entries as [FilterOperand, string]; + const valueConditional = + operand === FilterOperand.like ? `%${value}%` : value; + const fieldWithAlias = this.getAliasPath(fieldName); + const paramsName = this.getParamName(fieldWithAlias); + + if (!isTargetField(this._relationFields, fieldName)) { + if ( + (operand === FilterOperand.ne || operand === FilterOperand.eq) && + (valueConditional === 'null' || valueConditional === null) + ) { + const expression = OperandMapExpressionForNull[operand].replace( + EXPRESSION, + paramsName + ); + resultExpression.push({ + alias: fieldWithAlias, + expression, + }); + continue; + } + + const expression = OperandsMapExpression[operand].replace( + EXPRESSION, + paramsName + ); + resultExpression.push({ + alias: fieldWithAlias, + expression, + params: { + val: valueConditional, + name: paramsName, + }, + }); + continue; + } + + const metadataRelation: TypeOrmRelationMetadata = + this._relationMetadata[fieldName]; + const relationTarget = metadataRelation.inverseEntityMetadata.target; + const relationAlias = this._relationAlias[fieldName]; + const subQuery = this.repository.createQueryBuilder().subQuery(); + + const resultOperand = + operand === FilterOperand.eq ? operand : FilterOperand.ne; + switch (metadataRelation.relationType) { + case 'many-to-many': { + const subQuerySql = this.getSubQueryForManyToMany(fieldName); + + const resultOperand = + operand === FilterOperand.eq ? operand : FilterOperand.ne; + + const expression = OperandsMapExpressionForNullRelation[ + resultOperand + ].replace(EXPRESSION, subQuerySql); + + resultExpression.push({ + expression, + }); + break; + } + case 'one-to-many': { + const joinColumn = metadataRelation.inverseSidePropertyPath; + + const aliasPath = this.getAliasPath(joinColumn, fieldName); + const subQuerySql = subQuery + .select(aliasPath, joinColumn) + .from(relationTarget, relationAlias) + .where( + `${aliasPath} = ${this.getAliasPath(this.currentPrimaryColumn)}` + ) + .getQuery(); + + const expression = OperandsMapExpressionForNullRelation[ + resultOperand + ].replace(EXPRESSION, subQuerySql); + + resultExpression.push({ + expression, + }); + break; + } + default: { + const expression = OperandMapExpressionForNull[ + resultOperand + ].replace(EXPRESSION, paramsName); + resultExpression.push({ + alias: fieldWithAlias, + expression, + }); + } + } + } + } + + return resultExpression; + } + + getFilterExpressionForRelation(query: Query): ResultQueryExpression[] { + const resultExpression: ResultQueryExpression[] = []; + const filterRelation = this.getFilterObject(query, 'relation'); + if (!filterRelation) return resultExpression; + + for (const [relationField, propsFilter] of ObjectTyped.entries( + filterRelation + )) { + if (!propsFilter) continue; + if (!isTargetField(this._relationFields, relationField)) continue; + const metadataRelation: TypeOrmRelationMetadata = + this._relationMetadata[relationField]; + + const conditionalForManyToMany: { + conditional: string; + params: { name: string; val: string } | undefined; + }[] = []; + + for (const [relationFieldProps, filter] of ObjectTyped.entries( + propsFilter + )) { + if (!filter) continue; + + for (const entries of ObjectTyped.entries(filter)) { + const [operand, value] = entries as [FilterOperand, string]; + const currentValue = + operand === FilterOperand.like ? `%${value}%` : value; + + const paramsName = this.getParamName( + this.getAliasPath(relationFieldProps.toString(), relationField) + ); + let expression: string; + if (value === 'null') { + const currentOperand = + operand === FilterOperand.eq + ? FilterOperand.eq + : FilterOperand.ne; + expression = OperandMapExpressionForNull[currentOperand]; + } else { + expression = OperandsMapExpression[operand].replace( + EXPRESSION, + paramsName + ); + } + + const params = + value === 'null' + ? undefined + : { + val: currentValue, + name: paramsName, + }; + + switch (metadataRelation.relationType) { + case 'many-to-many': { + conditionalForManyToMany.push({ + params, + conditional: `${this.getAliasPath( + relationFieldProps.toString(), + this.getAliasForRelation(relationField) + )} ${expression}`, + }); + + break; + } + default: { + resultExpression.push({ + alias: this.getAliasPath( + relationFieldProps.toString(), + this.getAliasForRelation(relationField) + ), + expression, + selectInclude: relationField, + params, + }); + } + } + } + } + + if (conditionalForManyToMany.length) { + const expression = conditionalForManyToMany.map((i) => i.conditional); + const subQuery = this.getSubQueryForManyToMany( + relationField, + expression + ); + + const mainExpression = `IN ${subQuery}`; + + const params = conditionalForManyToMany + .filter((i) => i.params) + .map((i) => i.params) as { name: string; val: string }[]; + resultExpression.push({ + alias: this.getAliasPath(this.currentPrimaryColumn), + expression: mainExpression, + paramsForResult: expression, + params, + }); + } + } + return resultExpression; + } + + private throwError(message: string, path: string[], key?: string) { + const error: ValidateQueryError = { + code: 'unrecognized_keys', + path, + message, + }; + if (key) { + error.keys = [key]; + } + throw new BadRequestException([error]); + } + + async *asyncIterateFindRelationships( + relationships: NonNullable< + PatchData['relationships'] | PostData['relationships'] + > + ): AsyncGenerator> { + for (const entries of ObjectTyped.entries(relationships)) { + const [props, dataItem] = entries; + + isRelationField(this._relationFields, props); + if (dataItem === undefined) continue; + + const { data } = dataItem; + if (data === undefined) continue; + if (data === null) { + yield { [props]: null } as RelationshipsResult; + continue; + } + const isArray = Array.isArray(data); + if (isArray && data.length === 0) { + yield { [props]: [] } as RelationshipsResult; + continue; + } + + const condition = isArray + ? In((data as any[]).map((i) => i.id)) + : Equal(data['id']); + const relationsTypeName = kebabToCamel( + isArray ? (data as any[])[0]['type'] : data['type'] + ); + const primaryField = this.getPrimaryColumnForRel( + props as TupleOfEntityRelation[number] + ); + const relationsTarget = + this._relationMetadata[props as TupleOfEntityRelation[number]] + .inverseEntityMetadata.target; + const result = await this.repository.manager + .getRepository(relationsTarget) + .find({ + select: { + [primaryField]: true, + }, + where: { + [primaryField]: condition, + }, + }); + + if ( + (isArray && (result.length === 0 || data.length !== result.length)) || + (!isArray && result.length === 0) + ) { + const message = isArray + ? `Resource '${relationsTypeName}' with ids '${(data as any[]) + .map((i) => i.id) + .filter((i) => !result.find((r) => r[primaryField] == i)) + .join(',')}' does not exist` + : `Resource '${relationsTypeName}' with id '${data.id}' does not exist`; + + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data', 'relationships', props.toString()], + message, + }; + + throw new BadRequestException([error]); + } + + yield { [props]: isArray ? result : result[0] } as RelationshipsResult; + } + } + + async saveEntityData( + target: E, + relationships: PatchData['relationships'] | PostData['relationships'] + ): Promise { + if (relationships) { + for await (const item of this.asyncIterateFindRelationships( + relationships + )) { + const [props, type] = ObjectTyped.entries(item)[0]; + if (type !== null) { + target[props] = type as any; + } else { + target[props] = null as any; + } + } + } + const saveData = await this.repository.save(target); + let saveDataWithRelation: E | null = null; + if (relationships) { + const queryBuilder = this.repository + .createQueryBuilder(this.currentAlias) + .where({ + [this.currentPrimaryColumn]: Equal( + saveData[this.currentPrimaryColumn] + ), + }); + + for (const [props] of ObjectTyped.entries(relationships)) { + const currentIncludeAlias = this.getAliasForRelation(props.toString()); + + queryBuilder.leftJoinAndSelect( + this.getAliasPath(props), + currentIncludeAlias + ); + } + + saveDataWithRelation = await queryBuilder.getOne(); + } + + return saveDataWithRelation ? saveDataWithRelation : saveData; + } + + async validateRelationInputData< + Rel extends EntityRelation, + In extends InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends null | InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends null | InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise> { + const relationMetadata = + this._relationMetadata[rel as TupleOfEntityRelation[number]]; + const isArray = Array.isArray(inputData); + + if ( + ['one-to-many', 'many-to-many'].includes(relationMetadata.relationType) && + !isArray + ) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data'], + message: 'Body data should be array', + }; + + throw new UnprocessableEntityException([error]); + } + + if ( + ['one-to-one', 'many-to-one'].includes(relationMetadata.relationType) && + isArray + ) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data'], + message: 'Body data should be object', + }; + + throw new UnprocessableEntityException([error]); + } + if (inputData === null) { + const result = null; + return result as ValidateReturn; + } + if (isArray && inputData.length === 0) { + const result: any[] = []; + return result as ValidateReturn; + } + + const prepareData = isArray ? inputData : [inputData]; + + const errors: ValidateQueryError[] = []; + let i = 0; + const typeName = camelToKebab( + getEntityName(relationMetadata.inverseEntityMetadata.target) + ); + + for (const prepareItem of prepareData) { + if (prepareItem.type !== typeName) { + const path = isArray ? ['data', i.toString()] : ['data']; + errors.push({ + code: 'invalid_arguments', + path: path, + message: `Type should be equal to type of relName: "${rel.toString()}". Type of ${rel.toString()} is "${typeName}" but receive - "${ + prepareItem.type + }"`, + }); + } + i++; + } + if (errors.length) { + throw new UnprocessableEntityException(errors); + } + + const checkResult = await this.repository.manager + .getRepository(relationMetadata.inverseEntityMetadata.target) + .find({ + select: { + [this.getPrimaryColumnForRel(rel.toString())]: true, + }, + where: { + [this.getPrimaryColumnForRel(rel.toString())]: In( + prepareData.map((i) => i.id) + ), + }, + }); + + if (checkResult.length === prepareData.length) { + return ( + isArray ? inputData.map((i) => i.id) : inputData.id + ) as ValidateReturn; + } + + const resulDataMap = checkResult.reduce((acum, item) => { + acum[item[this.getPrimaryColumnForRel(rel.toString())]] = true; + return acum; + }, {} as Record); + + i = 0; + for (const item of prepareData) { + if (!resulDataMap[item.id]) { + const path = isArray ? ['data', i.toString(), 'id'] : ['data', 'id']; + errors.push({ + code: 'invalid_arguments', + path: path, + message: `Not exist item "${ + item.id + }" in relation "${rel.toString()}"`, + }); + } + i++; + } + throw new NotFoundException(errors); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts new file mode 100644 index 00000000..715d1c86 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts @@ -0,0 +1,67 @@ +import { DynamicModule } from '@nestjs/common'; +import { TypeOrmModule as MainTypeOrmModule } from '@nestjs/typeorm'; +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; + +import { NestProvider, ObjectLiteral, ResultModuleOptions } from '../../types'; +import { + CurrentEntityManager, + CurrentDataSourceProvider, + ZodParamsFactory, + CurrentEntityRepository, + FindOneRowEntityFactory, + CheckRelationNameFactory, + OrmServiceFactory, + GetFieldForEntity, + RunInTransactionFactory, +} from './factory'; +import { + EntityPropsMapService, + TransformDataService, + TypeormUtilsService, +} from './service'; +import { GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; + +export class TypeOrmModule { + static forRoot(options: ResultModuleOptions): DynamicModule { + const optionProvider = { + provide: GLOBAL_MODULE_OPTIONS_TOKEN, + useValue: options, + }; + + const typeOrmModule = MainTypeOrmModule.forFeature( + options.entities as EntityClassOrSchema[], + options.connectionName + ); + + const currentProvider = [ + ...(options.providers || []), + optionProvider, + CurrentDataSourceProvider(options.connectionName), + CurrentEntityManager(), + GetFieldForEntity(), + EntityPropsMapService, + RunInTransactionFactory(), + ]; + + const currentImport = [typeOrmModule, ...(options.imports || [])]; + + return { + module: TypeOrmModule, + imports: currentImport, + providers: currentProvider, + exports: [...currentProvider, ...currentImport], + }; + } + + static getUtilProviders(entity: ObjectLiteral): NestProvider { + return [ + CurrentEntityRepository(entity), + TransformDataService, + TypeormUtilsService, + ZodParamsFactory(), + OrmServiceFactory(), + FindOneRowEntityFactory(), + CheckRelationNameFactory(), + ]; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts new file mode 100644 index 00000000..7494c8e4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts @@ -0,0 +1,11 @@ +import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; +import { EntityTarget, ObjectLiteral } from '../../types'; +import { ResultGetField } from '../mixin/types'; + +export type TypeOrmParam = { + useSoftDelete?: boolean; + runInTransaction?: any>( + isolationLevel: IsolationLevel, + fn: Func + ) => ReturnType; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/config-param.ts b/libs/json-api/json-api-nestjs/src/lib/types/config-param.ts new file mode 100644 index 00000000..4d3c05e5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/config-param.ts @@ -0,0 +1,45 @@ +import { + AnyEntity, + EntityName, + NestController, + NestImport, + NestProvider, + PipeMixin, +} from './util-types'; +import { NonEmptyArray } from 'zod-validation-error'; +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; + +export type ExtractNestType = + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; + +export type ConfigParam = { + requiredSelectField: boolean; + debug: boolean; + pipeForId: PipeMixin; + operationUrl: string; + overrideRoute: string; +}; + +export type GeneralParam = { + connectionName?: string; + entities: NonEmptyArray>; + controllers?: NestController; + providers?: NestProvider; + imports?: NestImport; +}; + +export type ResultGeneralParam = { + connectionName: string; + entities: NonEmptyArray>; + controllers: NestController; + providers: NestProvider; + imports: NestImport; +}; + +export interface BaseModuleOptions { + entity: EntityClassOrSchema; + connectionName: string; + controller?: ExtractNestType; + config: ConfigParam; + imports?: NestImport; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts new file mode 100644 index 00000000..ca9e3564 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts @@ -0,0 +1,18 @@ +import { ZodIssue } from 'zod'; + +export type InnerErrorType = + | 'invalid_arguments' + | 'unrecognized_keys' + | 'internal_error'; + +export type InnerError = { + code: InnerErrorType; + message: string; + path: string[]; + keys?: string[]; + error?: Error; +}; + +export type ValidateQueryError = ZodIssue | InnerError; + +export type ErrorDescribe = ValidateQueryError; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/types/index.ts new file mode 100644 index 00000000..0ad37ef9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/index.ts @@ -0,0 +1,5 @@ +export * from './config-param'; +export * from './module-common.types'; +export * from './util-types'; +export * from './error.types'; +export * from './operand'; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts new file mode 100644 index 00000000..8c83dd54 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts @@ -0,0 +1,29 @@ +import { + MicroOrmModule, + TypeOrmModule, + TypeOrmParam, + MicroOrmParam, +} from '../modules'; + +import { ConfigParam, GeneralParam, ResultGeneralParam } from './config-param'; +import { RequiredFromPartial } from './util-types'; + +export type ModuleOptions = + | (GeneralParam & { + type: typeof MicroOrmModule; + options: Partial; + }) + | (GeneralParam & { + type?: typeof TypeOrmModule; + options: Partial; + }); + +export type ResultModuleOptions = + | (ResultGeneralParam & { + type: typeof MicroOrmModule; + options: RequiredFromPartial; + }) + | (ResultGeneralParam & { + type: typeof TypeOrmModule; + options: RequiredFromPartial; + }); diff --git a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts b/libs/json-api/json-api-nestjs/src/lib/types/operand.ts new file mode 100644 index 00000000..c54b9985 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/operand.ts @@ -0,0 +1,26 @@ +import { FilterOperand } from '@klerick/json-api-nestjs-shared'; + +export const EXPRESSION = 'EXPRESSION'; +export const OperandsMapExpression = { + [FilterOperand.eq]: `= :${EXPRESSION}`, + [FilterOperand.ne]: `<> :${EXPRESSION}`, + [FilterOperand.regexp]: `~* :${EXPRESSION}`, + [FilterOperand.gt]: `> :${EXPRESSION}`, + [FilterOperand.gte]: `>= :${EXPRESSION}`, + [FilterOperand.in]: `IN (:...${EXPRESSION})`, + [FilterOperand.like]: `ILIKE :${EXPRESSION}`, + [FilterOperand.lt]: `< :${EXPRESSION}`, + [FilterOperand.lte]: `<= :${EXPRESSION}`, + [FilterOperand.nin]: `NOT IN (:...${EXPRESSION})`, + [FilterOperand.some]: `&& :${EXPRESSION}`, +}; + +export const OperandMapExpressionForNull = { + [FilterOperand.ne]: 'IS NOT NULL', + [FilterOperand.eq]: 'IS NULL', +}; + +export const OperandsMapExpressionForNullRelation = { + [FilterOperand.ne]: `EXISTS ${EXPRESSION}`, + [FilterOperand.eq]: `NOT EXISTS ${EXPRESSION}`, +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/util-types.ts b/libs/json-api/json-api-nestjs/src/lib/types/util-types.ts new file mode 100644 index 00000000..1954c168 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/util-types.ts @@ -0,0 +1,24 @@ +import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common'; +import { EntityTarget as EntityTargetTypeOrm } from 'typeorm/common/EntityTarget'; + +export type AnyEntity = Partial; + +export type EntityClass = Function & { prototype: T }; +export type EntityName = EntityClass; +export type EntityTarget = EntityClass | EntityTargetTypeOrm; +export interface ObjectLiteral { + [key: string]: any; +} + +export type NestController = NonNullable; +export type NestProvider = NonNullable; +export type NestImport = NonNullable; +export type PipeMixin = Type; + +export type RequiredFromPartial = { + [P in keyof T]-?: T[P] extends infer U | undefined ? U | false : T[P]; +}; + +export type RunInTransaction< + F extends (...arg: any[]) => Promise = (...arg: any[]) => Promise +> = (arg: F) => ReturnType; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts new file mode 100644 index 00000000..ca67bf72 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts @@ -0,0 +1,152 @@ +import { DynamicModule, ParseIntPipe } from '@nestjs/common'; + +import { MicroOrmModule, TypeOrmModule, TypeOrmParam } from '../modules'; +import { ConfigParam, RequiredFromPartial } from '../types'; +import { prepareConfig, createMixinModule } from './helper'; +import { + DEFAULT_CONNECTION_NAME, + JSON_API_CONTROLLER_POSTFIX, +} from '../constants'; +import { JsonBaseController } from '../modules/mixin/controller/json-base.controller'; +import { JsonApi } from '../modules/mixin/decorators'; +import { getProviderName } from '../modules/mixin/helper'; + +class A {} +describe('Helper tests', () => { + describe('prepareConfig', () => { + it('should return default config when type is undefined', () => { + const result = prepareConfig({ + entities: [A], + options: { debug: false, requiredSelectField: false }, + }); + + expect(Array.isArray(result.imports)).toBe(true); + expect(Array.isArray(result.controllers)).toBe(true); + expect(Array.isArray(result.providers)).toBe(true); + expect(result.type).toBe(TypeOrmModule); + expect(result.options.debug).toBe(false); + expect(result.options.requiredSelectField).toBe(false); + expect(result.connectionName).toBe(DEFAULT_CONNECTION_NAME); + }); + + it('should return TypeOrm config when type is TypeOrmModule', () => { + const result = prepareConfig({ + entities: [A], + type: TypeOrmModule, + options: { + debug: true, + requiredSelectField: true, + useSoftDelete: true, + }, + }); + + expect(result.type).toBe(TypeOrmModule); + expect(result.options.debug).toBe(true); + expect( + (result.options as RequiredFromPartial) + .useSoftDelete + ).toBe(true); + }); + + it('should return MicroOrm config when type is MicroOrmModule', () => { + const result = prepareConfig({ + entities: [A], + type: MicroOrmModule, + options: { debug: true, requiredSelectField: true }, + }); + + expect(result.type).toBe(MicroOrmModule); + expect(result.options.debug).toBe(true); + expect(result.options.requiredSelectField).toBe(true); + + // @ts-expect-error eed check run time + expect((result.options as ConfigParam).useSoftDelete).toBeUndefined(); + }); + + it('should use default values for pipeForId, operationUrl, and overrideRoute when not provided', () => { + const result = prepareConfig({ + entities: [A], + options: {}, + }); + + expect(result.options.pipeForId).toBe(ParseIntPipe); + expect(result.options.operationUrl).toBe(false); + expect(result.options.overrideRoute).toBe(false); + }); + }); + + describe('createMixinModule', () => { + it('should create a MixinModule with the correct controller matching the entity', () => { + class TestEntity {} + @JsonApi(TestEntity) + class TestController extends JsonBaseController {} + const commonOrmModule = {} as DynamicModule; + const resultOptions = prepareConfig({ + entities: [TestEntity], + controllers: [TestController], + connectionName: DEFAULT_CONNECTION_NAME, + type: TypeOrmModule, + options: { + debug: true, + requiredSelectField: true, + useSoftDelete: true, + }, + }); + + const result = createMixinModule( + TestEntity, + resultOptions, + commonOrmModule + ); + + expect(result).toHaveProperty('controllers', [TestController]); + expect(result).toHaveProperty('providers'); + expect(result.imports?.includes(commonOrmModule)).toBe(true); + }); + + it('should use undefined as controller if none match the entity', () => { + class TestEntity {} + const commonOrmModule = {} as DynamicModule; + const resultOptions = prepareConfig({ + entities: [TestEntity], + controllers: [], + connectionName: 'test_connection', + options: { debug: false }, + imports: [], + }); + + const result = createMixinModule( + TestEntity, + resultOptions, + commonOrmModule + ); + + const controller = (result.controllers || []).at(0); + expect(controller?.name).toBe( + getProviderName(TestEntity, JSON_API_CONTROLLER_POSTFIX) + ); + }); + + it('should correctly construct the MixinModule using given ResultModuleOptions', () => { + class AnotherEntity {} + class SharedModule {} + const commonOrmModule = {} as DynamicModule; + const importTest = { module: SharedModule }; + + const resultOptions = prepareConfig({ + entities: [AnotherEntity], + controllers: [], + connectionName: 'default_connection', + options: { debug: true, useSoftDelete: true }, + imports: [importTest], + }); + + const result = createMixinModule( + AnotherEntity, + resultOptions, + commonOrmModule + ); + expect(result.imports?.at(1)).toEqual(importTest); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts b/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts new file mode 100644 index 00000000..aa2154d5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts @@ -0,0 +1,120 @@ +import { DynamicModule, ParseIntPipe } from '@nestjs/common'; + +import { + AnyEntity, + ConfigParam, + EntityName, + ModuleOptions, + RequiredFromPartial, + ResultModuleOptions, +} from '../types'; +import { + DEFAULT_CONNECTION_NAME, + JSON_API_DECORATOR_ENTITY, +} from '../constants'; +import { + MicroOrmParam, + TypeOrmParam, + MicroOrmModule, + TypeOrmModule, + AtomicOperationModule, +} from '../modules'; +import { MixinModule } from '../modules/mixin/mixin.module'; +import { Type } from '@nestjs/common/interfaces'; +import { RouterModule } from '@nestjs/core'; + +export function prepareConfig( + moduleOptions: ModuleOptions +): ResultModuleOptions { + const { options: inputOptions } = moduleOptions; + + let resulOptions: + | RequiredFromPartial + | RequiredFromPartial; + let resulType: typeof TypeOrmModule | typeof MicroOrmModule; + const configParam: RequiredFromPartial = { + debug: !!inputOptions.debug, + requiredSelectField: !!inputOptions.requiredSelectField, + operationUrl: inputOptions.operationUrl || false, + overrideRoute: inputOptions.overrideRoute || false, + pipeForId: inputOptions.pipeForId || ParseIntPipe, + }; + + moduleOptions.type = moduleOptions.type || TypeOrmModule; + + if (moduleOptions.type === TypeOrmModule) { + const { runInTransaction, useSoftDelete } = + moduleOptions.options as Partial; + + resulType = TypeOrmModule; + resulOptions = { + ...configParam, + useSoftDelete: useSoftDelete ? useSoftDelete : false, + runInTransaction: runInTransaction ? runInTransaction : false, + } as ConfigParam & RequiredFromPartial; + } else { + resulType = MicroOrmModule; + resulOptions = { + ...configParam, + }; + } + + return { + connectionName: moduleOptions.connectionName || DEFAULT_CONNECTION_NAME, + entities: moduleOptions.entities, + imports: moduleOptions.imports || [], + providers: moduleOptions.providers || [], + controllers: moduleOptions.controllers || [], + type: resulType, + options: resulOptions, + } satisfies ResultModuleOptions; +} + +export function createMixinModule( + entity: EntityName, + resultOption: ResultModuleOptions, + commonOrmModule: DynamicModule +): DynamicModule { + const controller = (resultOption.controllers || []).find( + (item) => + item && Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, item) === entity + ); + + return MixinModule.forRoot({ + entity, + controller, + config: resultOption.options, + imports: [commonOrmModule, ...resultOption.imports], + ormModule: resultOption.type, + }); +} + +export function createAtomicModule( + options: ResultModuleOptions, + entitiesMixinModules: DynamicModule[], + commonOrmModule: DynamicModule +): DynamicModule[] { + const { operationUrl } = options.options; + if (!operationUrl) return []; + + return [ + AtomicOperationModule.forRoot( + { + ...options, + connectionName: options.connectionName, + }, + entitiesMixinModules, + commonOrmModule + ), + RouterModule.register([ + { + module: AtomicOperationModule, + path: operationUrl, + }, + ]), + ]; +} + +export function entityForClass(type: Type): EntityName { + return Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, type); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/utils/index.ts new file mode 100644 index 00000000..d6d6f056 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/index.ts @@ -0,0 +1 @@ +export * from './helper'; diff --git a/migrations.json b/migrations.json new file mode 100644 index 00000000..a153a5fe --- /dev/null +++ b/migrations.json @@ -0,0 +1,142 @@ +{ + "migrations": [ + { + "version": "20.0.0-beta.7", + "description": "Migration for v20.0.0-beta.7", + "implementation": "./src/migrations/update-20-0-0/move-use-daemon-process", + "package": "nx", + "name": "move-use-daemon-process" + }, + { + "version": "20.0.1", + "description": "Set `useLegacyCache` to true for migrating workspaces", + "implementation": "./src/migrations/update-20-0-1/use-legacy-cache", + "x-repair-skip": true, + "package": "nx", + "name": "use-legacy-cache" + }, + { + "cli": "nx", + "version": "20.0.0-beta.5", + "description": "replace getJestProjects with getJestProjectsAsync", + "implementation": "./src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync", + "package": "@nx/jest", + "name": "replace-getJestProjects-with-getJestProjectsAsync" + }, + { + "version": "20.2.0-beta.5", + "description": "Update TypeScript ESLint packages to v8.13.0 if they are already on v8", + "implementation": "./src/migrations/update-20-2-0/update-typescript-eslint-v8-13-0", + "package": "@nx/eslint", + "name": "update-typescript-eslint-v8.13.0" + }, + { + "cli": "nx", + "version": "19.6.1-beta.0", + "description": "Ensure Target Defaults are set correctly for Module Federation.", + "factory": "./src/migrations/update-19-6-1/ensure-depends-on-for-mf", + "package": "@nx/angular", + "name": "update-19-6-1-ensure-module-federation-target-defaults" + }, + { + "cli": "nx", + "version": "20.2.0-beta.2", + "description": "Update the ModuleFederationConfig import use @nx/module-federation.", + "factory": "./src/migrations/update-20-2-0/migrate-mf-imports-to-new-package", + "package": "@nx/angular", + "name": "update-20-2-0-update-module-federation-config-import" + }, + { + "cli": "nx", + "version": "20.2.0-beta.2", + "description": "Update the withModuleFederation import use @nx/module-federation/angular.", + "factory": "./src/migrations/update-20-2-0/migrate-with-mf-import-to-new-package", + "package": "@nx/angular", + "name": "update-20-2-0-update-with-module-federation-import" + }, + { + "cli": "nx", + "version": "20.2.0-beta.5", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Update the @angular/cli package version to ~19.0.0.", + "factory": "./src/migrations/update-20-2-0/update-angular-cli", + "package": "@nx/angular", + "name": "update-angular-cli-version-19-0-0" + }, + { + "cli": "nx", + "version": "20.2.0-beta.5", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Add the '@angular/localize/init' polyfill to the 'polyfills' option of targets using esbuild-based executors.", + "factory": "./src/migrations/update-20-2-0/add-localize-polyfill-to-targets", + "package": "@nx/angular", + "name": "add-localize-polyfill-to-targets" + }, + { + "cli": "nx", + "version": "20.2.0-beta.5", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Update '@angular/ssr' import paths to use the new '/node' entry point when 'CommonEngine' is detected.", + "factory": "./src/migrations/update-20-2-0/update-angular-ssr-imports-to-use-node-entry-point", + "package": "@nx/angular", + "name": "update-angular-ssr-imports-to-use-node-entry-point" + }, + { + "cli": "nx", + "version": "20.2.0-beta.6", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Disable the Angular ESLint prefer-standalone rule if not set.", + "factory": "./src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone", + "package": "@nx/angular", + "name": "disable-angular-eslint-prefer-standalone" + }, + { + "cli": "nx", + "version": "20.2.0-beta.8", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Remove Angular ESLint rules that were removed in v19.0.0.", + "factory": "./src/migrations/update-20-2-0/remove-angular-eslint-rules", + "package": "@nx/angular", + "name": "remove-angular-eslint-rules" + }, + { + "cli": "nx", + "version": "20.2.0-beta.8", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Remove the deprecated 'tailwindConfig' option from ng-packagr executors. Tailwind CSS configurations located at the project or workspace root will be picked up automatically.", + "factory": "./src/migrations/update-20-2-0/remove-tailwind-config-from-ng-packagr-executors", + "package": "@nx/angular", + "name": "remove-tailwind-config-from-ng-packagr-executors" + }, + { + "cli": "nx", + "version": "19.6.3-beta.0", + "description": "Migrate proxy config files to match new format from webpack-dev-server v5.", + "implementation": "./src/migrations/update-19-6-3/proxy-config", + "package": "@nx/webpack", + "name": "update-19-6-3-proxy-config" + }, + { + "version": "19.0.0", + "description": "Updates non-standalone Directives, Component and Pipes to 'standalone:false' and removes 'standalone:true' from those who are standalone", + "factory": "./bundles/explicit-standalone-flag#migrate", + "package": "@angular/core", + "name": "explicit-standalone-flag" + }, + { + "version": "19.0.0", + "description": "Updates ExperimentalPendingTasks to PendingTasks", + "factory": "./bundles/pending-tasks#migrate", + "package": "@angular/core", + "name": "pending-tasks" + }, + { + "version": "19.0.0", + "description": "Replaces `APP_INITIALIZER`, `ENVIRONMENT_INITIALIZER` & `PLATFORM_INITIALIZER` respectively with `provideAppInitializer`, `provideEnvironmentInitializer` & `providePlatformInitializer`.", + "factory": "./bundles/provide-initializer#migrate", + "optional": true, + "package": "@angular/core", + "name": "provide-initializer" + } + ] +} diff --git a/project.json b/project.json new file mode 100644 index 00000000..5b9f7ecd --- /dev/null +++ b/project.json @@ -0,0 +1,14 @@ +{ + "name": "@nestjs-json-api/source", + "$schema": "node_modules/nx/schemas/project-schema.json", + "targets": { + "local-registry": { + "executor": "@nx/js:verdaccio", + "options": { + "port": 4873, + "config": ".verdaccio/config.yml", + "storage": "tmp/local-registry/storage" + } + } + } +} From 3e828a5faa74454bea9a1af8e4ffe5d3efc06b56 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 06:59:32 +0100 Subject: [PATCH 03/26] refactor(json-api-nestjs-sdk): Rename npm package and fox data BREAKING CHANGE: Rename npm package from json-api-nestjs-sdk to @klerick/json-api-nestjs-sdk. Now relation body params allow only as specification. https://jsonapi.org/format/#crud-updating-resource-relationships befoare allow without data props --- .../json-api/json-api-sdk/atomic-sdk.spec.ts | 2 +- .../check-common-decorator.spec.ts | 2 +- .../json-api-sdk/check-othe-call.spec.ts | 2 +- .../json-api/json-api-sdk/get-method.spec.ts | 3 +- .../json-api-sdk/patch-methode.spec.ts | 2 +- .../json-api/json-api-sdk/post-method.spec.ts | 2 +- .../src/json-api/utils/run-application.ts | 2 +- libs/json-api/json-api-nestjs-sdk/README.md | 10 +- .../json-api/json-api-nestjs-sdk/package.json | 2 +- .../src/lib/types/entity.ts | 2 +- .../src/lib/types/filter-operand.ts | 2 +- .../src/lib/types/response-body.ts | 2 +- .../src/lib/types/utils.ts | 2 +- .../lib/utils/generate-atomic-body.spec.ts | 28 +- .../src/lib/utils/index.ts | 4 +- nx.json | 1 + package-lock.json | 484 ++++++++++++++++-- package.json | 10 +- tsconfig.base.json | 21 +- 19 files changed, 487 insertions(+), 96 deletions(-) diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts index bb5fe060..9551718b 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts @@ -1,5 +1,5 @@ import { INestApplication } from '@nestjs/common'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { Addresses, CommentKind, Comments, Roles, Users } from 'database'; import { faker } from '@faker-js/faker'; import { getUser } from '../utils/data-utils'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts index 47b18d9f..91eb763e 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts @@ -1,5 +1,5 @@ import { INestApplication } from '@nestjs/common'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { AxiosError } from 'axios'; import { Users } from 'database'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts index 076dc277..abf3c40d 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts @@ -1,5 +1,5 @@ import { INestApplication } from '@nestjs/common'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { BookList, Users } from 'database'; import { AxiosError } from 'axios'; import { faker } from '@faker-js/faker'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts index 04889c41..d64f62cb 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts @@ -2,10 +2,9 @@ import { INestApplication } from '@nestjs/common'; import { Addresses, CommentKind, Comments, Roles, Users } from 'database'; import { faker } from '@faker-js/faker'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { getUser } from '../utils/data-utils'; import { creatSdk, run } from '../utils/run-application'; -import { AxiosError } from 'axios'; let app: INestApplication; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts index 62deb265..720828d9 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts @@ -1,7 +1,7 @@ import { INestApplication } from '@nestjs/common'; import { Addresses, CommentKind, Comments, Users } from 'database'; import { faker } from '@faker-js/faker'; -import { JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { creatSdk, run } from '../utils/run-application'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts index ebc9b484..e20b86d9 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts @@ -1,6 +1,6 @@ import { Addresses, BookList, CommentKind, Comments, Users } from 'database'; import { faker } from '@faker-js/faker'; -import { JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { creatSdk, run } from '../utils/run-application'; import { INestApplication } from '@nestjs/common'; diff --git a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts index b4e57aca..74698971 100644 --- a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts +++ b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import { adapterForAxios, JsonApiJs } from 'json-api-nestjs-sdk'; +import { adapterForAxios, JsonApiJs } from '@klerick/json-api-nestjs-sdk'; import { RpcFactory, axiosTransportFactory, diff --git a/libs/json-api/json-api-nestjs-sdk/README.md b/libs/json-api/json-api-nestjs-sdk/README.md index 15f7fbc4..51cc5bf0 100644 --- a/libs/json-api/json-api-nestjs-sdk/README.md +++ b/libs/json-api/json-api-nestjs-sdk/README.md @@ -14,7 +14,7 @@ The plugin of client for help work with JSON API over [json-api-nestjs](https:// ## Installation ```bash $ -npm install json-api-nestjs-sdk +npm install @klerick/json-api-nestjs-sdk ``` ## Example @@ -27,7 +27,7 @@ import { FilterOperand, JsonApiJs, JsonSdkPromise, -} from 'json-api-nestjs-sdk'; +} from '@klerick/json-api-nestjs-sdk'; import { faker } from '@faker-js/faker'; import axios from 'axios'; @@ -93,7 +93,11 @@ const [addressPost, managerPost, rolesPost, userPost] = await jsonSdk ``` or you can use Angular module: ```typescript -import { provideJsonApi, AtomicFactory, JsonApiSdkService } from 'json-api-nestjs-sdk/ngModule'; +import { + provideJsonApi, + AtomicFactory, + JsonApiSdkService +} from '@klerick/json-api-nestjs-sdk/ngModule'; import { provideHttpClient, withFetch, diff --git a/libs/json-api/json-api-nestjs-sdk/package.json b/libs/json-api/json-api-nestjs-sdk/package.json index f8169586..2f826053 100644 --- a/libs/json-api/json-api-nestjs-sdk/package.json +++ b/libs/json-api/json-api-nestjs-sdk/package.json @@ -1,5 +1,5 @@ { - "name": "json-api-nestjs-sdk", + "name": "@klerick/json-api-nestjs-sdk", "version": "7.0.1", "engines": { "node": ">= 16.0.0" diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts index ecc3a2b1..6a2d03de 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts @@ -1,4 +1,4 @@ -import { EntityField, EntityProps } from 'json-shared-type'; +import { EntityField, EntityProps } from '@klerick/json-api-nestjs-shared'; export { EntityField, EntityProps }; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts index ba47b6e3..6e161821 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts @@ -1,4 +1,4 @@ -import { FilterOperand } from 'json-shared-type'; +import { FilterOperand } from '@klerick/json-api-nestjs-shared'; export { FilterOperand }; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts index 33f67dab..fbd6a440 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts @@ -10,4 +10,4 @@ export { ResourceData, ResourceObject, ResourceObjectRelationships, -} from 'json-shared-type'; +} from '@klerick/json-api-nestjs-shared'; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts index 4ce83427..e6a8a2a4 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts @@ -1,4 +1,4 @@ -import { TypeOfArray } from 'json-shared-type'; +import { TypeOfArray } from '@klerick/json-api-nestjs-shared'; export { TypeOfArray }; type IntersectionToObj = { diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts index f768ed57..eec0812d 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts @@ -69,12 +69,14 @@ describe('GenerateAtomicBody', () => { text: entity.text, }, relationships: { - users: [ - { - id: `${user.id}`, - type: 'users', - }, - ], + users: { + data: [ + { + id: `${user.id}`, + type: 'users', + }, + ], + }, }, type: 'book-list', }, @@ -102,12 +104,14 @@ describe('GenerateAtomicBody', () => { text: entity.text, }, relationships: { - users: [ - { - id: `${user.id}`, - type: 'users', - }, - ], + users: { + data: [ + { + id: `${user.id}`, + type: 'users', + }, + ], + }, }, type: 'book-list', }, diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts index 1367c3c2..2436ccc3 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts @@ -1,4 +1,4 @@ -import { camelToKebab } from 'shared-utils'; +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; import { JsonApiSdkConfig, JsonSdkConfig } from '../types'; import { ID_KEY } from '../constants'; @@ -14,7 +14,7 @@ export { capitalizeFirstChar, kebabToCamel, isObject, -} from 'shared-utils'; +} from '@klerick/json-api-nestjs-shared'; export function resultConfig(partialConfig: JsonSdkConfig): JsonApiSdkConfig { return { diff --git a/nx.json b/nx.json index 4cd992f1..9a865fa3 100644 --- a/nx.json +++ b/nx.json @@ -89,6 +89,7 @@ "!type-for-rpc" ], "version": { + "preVersionCommand": "npx nx run-many -t build", "conventionalCommits": true, "generatorOptions": { "fallbackCurrentVersionResolver": "1.0.0" diff --git a/package-lock.json b/package-lock.json index 1f80a0a5..6087b0a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,10 @@ "@angular/platform-browser": "19.0.3", "@angular/platform-browser-dynamic": "19.0.3", "@angular/router": "19.0.3", + "@mikro-orm/core": "^6.4.3", + "@mikro-orm/mysql": "^6.4.3", + "@mikro-orm/nestjs": "^6.0.2", + "@mikro-orm/postgresql": "^6.4.3", "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", @@ -39,8 +43,8 @@ "tslib": "^2.3.0", "typeorm": "^0.3.20", "uuid": "^10.0.0", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0", "zone.js": "0.15.0" }, "devDependencies": { @@ -83,7 +87,7 @@ "eslint-config-prettier": "^9.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "jest-environment-node": "^29.4.1", + "jest-environment-node": "^29.7.0", "jest-preset-angular": "14.4.2", "jsonc-eslint-parser": "^2.1.0", "ng-packagr": "19.0.1", @@ -4469,6 +4473,188 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==" }, + "node_modules/@mikro-orm/core": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.3.tgz", + "integrity": "sha512-UTaqKs1bomYtGmEEZ8sNBOmW2OqT5NcMh+pBV2iJ6WLM5MuiIEuNhDMuvvPE5gNEwUzc1HyRhUV87bRDhDIGRg==", + "dependencies": { + "dataloader": "2.2.3", + "dotenv": "16.4.7", + "esprima": "4.0.1", + "fs-extra": "11.2.0", + "globby": "11.1.0", + "mikro-orm": "6.4.3", + "reflect-metadata": "0.2.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/b4nan" + } + }, + "node_modules/@mikro-orm/core/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@mikro-orm/core/node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/@mikro-orm/knex": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.3.tgz", + "integrity": "sha512-gVkRD/cIn6qxk/P9nR+IufZxJwuCCdv0AtcGvShxXXvaoIrQPJYDV7HRxBOHCEyNygr6M3Fqpph1oPoT6aezTQ==", + "dependencies": { + "fs-extra": "11.2.0", + "knex": "3.1.0", + "sqlstring": "2.3.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0", + "better-sqlite3": "*", + "libsql": "*", + "mariadb": "*" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "libsql": { + "optional": true + }, + "mariadb": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/knex/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@mikro-orm/mysql": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.3.tgz", + "integrity": "sha512-ZkrrzOWE9ouifU331q70K9BfAOD9SFRiNLNnECnzVrvDPWnthMV0ahGium9HyHpG4nev0Ybg6vnvq9IQzW1brg==", + "dependencies": { + "@mikro-orm/knex": "6.4.3", + "mysql2": "3.12.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "node_modules/@mikro-orm/nestjs": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@mikro-orm/nestjs/-/nestjs-6.0.2.tgz", + "integrity": "sha512-3bUBnJ0HwwIjsNPb0n7dfTWayqA5iSLHv/zM9juyBADSpJB2oBAd96QlUqDvNi/RckSg0m+ifLVT0Uw8e0+POg==", + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0 || ^6.0.0-dev.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@mikro-orm/postgresql": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/postgresql/-/postgresql-6.4.3.tgz", + "integrity": "sha512-3cGi1gW6ME3SyuRRiJmSBtzHFa6Kavy6bK9rsSAAfXz+Pso6UBsqvesATbruKxDF7/CLdQlIY3CZZHXksUIrQg==", + "dependencies": { + "@mikro-orm/knex": "6.4.3", + "pg": "8.13.1", + "postgres-array": "3.0.2", + "postgres-date": "2.1.0", + "postgres-interval": "4.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/@mikro-orm/postgresql/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/postgres-interval": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-4.0.2.tgz", + "integrity": "sha512-EMsphSQ1YkQqKZL2cuG0zHkmjCCzQqQ71l2GXITqRwjhRleCdv00bDk/ktaSi0LnlaPzAc3535KTrjXsTdtx7A==", + "engines": { + "node": ">=12" + } + }, "node_modules/@module-federation/bridge-react-webpack-plugin": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.7.6.tgz", @@ -5554,7 +5740,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -5567,7 +5752,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -5576,7 +5760,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -10746,7 +10929,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "engines": { "node": ">=8" } @@ -10843,6 +11025,14 @@ "node": "*" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aws4": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", @@ -11409,7 +11599,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -12923,6 +13112,11 @@ "node": ">=12" } }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==" + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -13062,6 +13256,14 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -13149,7 +13351,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -13256,9 +13457,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "engines": { "node": ">=12" }, @@ -14010,6 +14211,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -14031,7 +14240,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -14337,7 +14545,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -14393,7 +14600,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -14471,7 +14677,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -14997,6 +15202,14 @@ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -15048,7 +15261,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, "engines": { "node": ">=8.0.0" } @@ -15065,6 +15277,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -15098,7 +15315,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -15195,7 +15411,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -15225,8 +15440,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -15788,7 +16002,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, "engines": { "node": ">= 4" } @@ -15962,7 +16175,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -15989,7 +16201,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -16030,7 +16241,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -16096,7 +16306,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -16146,6 +16355,11 @@ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "dev": true }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -17566,7 +17780,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -17723,6 +17936,77 @@ "node": ">= 8" } }, + "node_modules/knex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/knex/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/knex/node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/koa": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", @@ -18475,6 +18759,11 @@ "node": ">=8.0" } }, + "node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==" + }, "node_modules/long-timeout": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", @@ -18528,6 +18817,20 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/luxon": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", @@ -18666,7 +18969,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -18683,7 +18985,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -18696,7 +18997,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -18704,6 +19004,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mikro-orm": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.3.tgz", + "integrity": "sha512-xDNzmLiL4EUTMOu9CbZ2d0sNIaUdH4RzDv4oqw27+u0/FPfvZTIagd+luxx1lWWqe/vg/iNtvqr5OcNQIYYrtQ==", + "engines": { + "node": ">= 18.12.0" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -19183,6 +19491,36 @@ "rimraf": "bin.js" } }, + "node_modules/mysql2": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", + "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -19193,6 +19531,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoclone": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", @@ -20681,8 +21038,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.11.1", @@ -20716,7 +21072,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -20851,17 +21206,17 @@ "dev": true }, "node_modules/pg-pool": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", - "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", - "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -21969,7 +22324,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -22137,7 +22491,6 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, "dependencies": { "resolve": "^1.20.0" }, @@ -22260,7 +22613,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -22302,7 +22654,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "engines": { "node": ">=8" } @@ -22390,7 +22741,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -22470,7 +22820,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -22720,6 +23069,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -22947,7 +23301,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -23295,6 +23648,14 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -23682,7 +24043,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -23876,6 +24236,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/terser": { "version": "5.36.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", @@ -24099,6 +24467,14 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -24118,7 +24494,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -24851,7 +25226,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -26748,17 +27122,17 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-validation-error": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.0.2.tgz", - "integrity": "sha512-21xGaDmnU7lJZ4J63n5GXWqi+rTzGy3gDHbuZ1jP6xrK/DEQGyOqs/xW7eH96tIfCOYm+ecCuT0bfajBRKEVUw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", "engines": { "node": ">=18.0.0" }, diff --git a/package.json b/package.json index 7bba1b85..2dc49ddf 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,10 @@ "@angular/platform-browser": "19.0.3", "@angular/platform-browser-dynamic": "19.0.3", "@angular/router": "19.0.3", + "@mikro-orm/core": "^6.4.3", + "@mikro-orm/mysql": "^6.4.3", + "@mikro-orm/nestjs": "^6.0.2", + "@mikro-orm/postgresql": "^6.4.3", "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", @@ -42,8 +46,8 @@ "tslib": "^2.3.0", "typeorm": "^0.3.20", "uuid": "^10.0.0", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0", "zone.js": "0.15.0" }, "devDependencies": { @@ -86,7 +90,7 @@ "eslint-config-prettier": "^9.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "jest-environment-node": "^29.4.1", + "jest-environment-node": "^29.7.0", "jest-preset-angular": "14.4.2", "jsonc-eslint-parser": "^2.1.0", "ng-packagr": "19.0.1", diff --git a/tsconfig.base.json b/tsconfig.base.json index ac808542..06afef18 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,18 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@klerick/json-api-nestjs": [ + "libs/json-api/json-api-nestjs/src/index.ts" + ], + "@klerick/json-api-nestjs-sdk": [ + "libs/json-api/json-api-nestjs-sdk/src/index.ts" + ], + "@klerick/json-api-nestjs-sdk/ngModule": [ + "libs/json-api/json-api-nestjs-sdk/src/ngModule.ts" + ], + "@klerick/json-api-nestjs-shared": [ + "libs/json-api/json-api-nestjs-shared/src/index.ts" + ], "@klerick/nestjs-json-rpc": [ "libs/json-rpc/nestjs-json-rpc/src/index.ts" ], @@ -25,14 +37,7 @@ "libs/json-rpc/nestjs-json-rpc-sdk/src/ngModule.ts" ], "@nestjs-json-api/type-for-rpc": ["libs/type-for-rpc/src/index.ts"], - "database": ["libs/database/src/index.ts"], - "json-api-nestjs": ["libs/json-api/json-api-nestjs/src/index.ts"], - "json-api-nestjs-sdk": ["libs/json-api/json-api-nestjs-sdk/src/index.ts"], - "json-api-nestjs-sdk/ngModule": [ - "libs/json-api/json-api-nestjs-sdk/src/ngModule.ts" - ], - "json-shared-type": ["libs/json-api/json-shared-type/src/index.ts"], - "shared-utils": ["libs/shared-utils/src/index.ts"] + "database": ["libs/database/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] From ccb835da0138d928836ddfda3ea1b08a361ee959 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 07:02:10 +0100 Subject: [PATCH 04/26] refactor(json-api-nestjs): Rename npm package and fox data BREAKING CHANGE: Rename npm package from json-api-nestjs to @klerick/json-api-nestjs --- .../extend-book-list/extend-book-list.controller.ts | 2 +- .../controllers/extend-user/extend-user.controller.ts | 2 +- apps/json-api-server/src/app/resources/resources.module.ts | 3 ++- .../src/app/resources/service/example.pipe.ts | 2 +- .../src/app/resources/service/guard.service.ts | 2 +- libs/json-api/json-api-nestjs/package.json | 2 +- libs/json-api/json-api-nestjs/tsconfig.json | 7 ++----- libs/json-api/json-api-nestjs/tsconfig.spec.json | 1 + 8 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts b/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts index 971bba19..612f74e3 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts +++ b/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts @@ -1,6 +1,6 @@ import { ParseUUIDPipe } from '@nestjs/common'; import { BookList } from 'database'; -import { JsonApi, JsonBaseController } from 'json-api-nestjs'; +import { JsonApi, JsonBaseController } from '@klerick/json-api-nestjs'; @JsonApi(BookList, { pipeForId: ParseUUIDPipe, diff --git a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts b/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts index 01acce2f..00c4ea1b 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts +++ b/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts @@ -19,7 +19,7 @@ import { PatchRelationshipData, ResourceObjectRelationships, PostData, -} from 'json-api-nestjs'; +} from '@klerick/json-api-nestjs'; import { ExamplePipe } from '../../service/example.pipe'; import { ExampleService } from '../../service/example.service'; import { ControllerInterceptor } from '../../service/controller.interceptor'; diff --git a/apps/json-api-server/src/app/resources/resources.module.ts b/apps/json-api-server/src/app/resources/resources.module.ts index 1c4d0e8d..6a6b5e2e 100644 --- a/apps/json-api-server/src/app/resources/resources.module.ts +++ b/apps/json-api-server/src/app/resources/resources.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { JsonApiModule } from 'json-api-nestjs'; +import { JsonApiModule, TypeOrmModule } from '@klerick/json-api-nestjs'; import { Users, Addresses, Comments, Roles, BookList } from 'database'; import { ExtendBookListController } from './controllers/extend-book-list/extend-book-list.controller'; import { ExtendUserController } from './controllers/extend-user/extend-user.controller'; @@ -11,6 +11,7 @@ import { ExampleService } from './service/example.service'; entities: [Users, Addresses, Comments, Roles, BookList], controllers: [ExtendBookListController, ExtendUserController], providers: [ExampleService], + type: TypeOrmModule, options: { debug: true, requiredSelectField: false, diff --git a/apps/json-api-server/src/app/resources/service/example.pipe.ts b/apps/json-api-server/src/app/resources/service/example.pipe.ts index 63d0dc9f..7cdf2399 100644 --- a/apps/json-api-server/src/app/resources/service/example.pipe.ts +++ b/apps/json-api-server/src/app/resources/service/example.pipe.ts @@ -4,7 +4,7 @@ import { PipeTransform, } from '@nestjs/common'; -import { Query } from 'json-api-nestjs'; +import { Query } from '@klerick/json-api-nestjs'; import { Users } from 'database'; export class ExamplePipe implements PipeTransform, Query> { diff --git a/apps/json-api-server/src/app/resources/service/guard.service.ts b/apps/json-api-server/src/app/resources/service/guard.service.ts index 66714d6e..a8567bc2 100644 --- a/apps/json-api-server/src/app/resources/service/guard.service.ts +++ b/apps/json-api-server/src/app/resources/service/guard.service.ts @@ -6,7 +6,7 @@ import { Injectable, } from '@nestjs/common'; import { Observable } from 'rxjs'; -import { entityForClass } from 'json-api-nestjs'; +import { entityForClass } from '@klerick/json-api-nestjs'; import { Request } from 'express'; import { Reflector } from '@nestjs/core'; diff --git a/libs/json-api/json-api-nestjs/package.json b/libs/json-api/json-api-nestjs/package.json index cee6a35c..b98c3663 100644 --- a/libs/json-api/json-api-nestjs/package.json +++ b/libs/json-api/json-api-nestjs/package.json @@ -1,5 +1,5 @@ { - "name": "json-api-nestjs", + "name": "@klerick/json-api-nestjs", "version": "7.0.4", "engines": { "node": ">= 16.0.0" diff --git a/libs/json-api/json-api-nestjs/tsconfig.json b/libs/json-api/json-api-nestjs/tsconfig.json index f22982be..0dc79caa 100644 --- a/libs/json-api/json-api-nestjs/tsconfig.json +++ b/libs/json-api/json-api-nestjs/tsconfig.json @@ -5,9 +5,9 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true }, "files": [], "include": [], @@ -15,9 +15,6 @@ { "path": "./tsconfig.lib.json" }, - { - "path": "./tsconfig-mjs.lib.json" - }, { "path": "./tsconfig.spec.json" } diff --git a/libs/json-api/json-api-nestjs/tsconfig.spec.json b/libs/json-api/json-api-nestjs/tsconfig.spec.json index 69a251f3..ebbb8e0d 100644 --- a/libs/json-api/json-api-nestjs/tsconfig.spec.json +++ b/libs/json-api/json-api-nestjs/tsconfig.spec.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "../../../dist/out-tsc", "module": "commonjs", + "moduleResolution": "node16", "types": ["jest", "node"] }, "include": [ From 77c7d03578cc08d5f40d3e32013ed29c2e35f625 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 07:05:28 +0100 Subject: [PATCH 05/26] chore: Change README.md Closes: #89 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 219146a1..2893a528 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ $ npm run seed:run ```bash # dev server -$ npm run demo:json-api +$ nx run json-api-server:serve:development ``` ## License From 84f69ab2ff9467299eec24ba8af4d2c0b3af1b35 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 06:36:34 +0100 Subject: [PATCH 06/26] refactor(json-api-nestjs): Remove old files preparation for adaptation for MikroOrm #97 --- libs/json-api/json-api-nestjs/src/index.ts | 19 - .../src/lib/config/bindings.spec.ts | 9 - .../src/lib/config/bindings.ts | 202 ----- .../src/lib/constants/defaults.ts | 23 - .../src/lib/constants/index.ts | 3 - .../src/lib/constants/postfix.ts | 7 - .../src/lib/constants/reflection.ts | 24 - .../src/lib/decorators/index.ts | 2 - .../inject-service.decorator.spec.ts | 31 - .../inject-service.decorator.ts | 7 - .../json-api/json-api.decorator.spec.ts | 68 -- .../decorators/json-api/json-api.decorator.ts | 19 - .../src/lib/factory/data-source.factory.ts | 14 - .../lib/factory/entity-repository.factory.ts | 25 - .../json-api-nestjs/src/lib/factory/index.ts | 5 - .../src/lib/factory/swagger-bind-method.ts | 8 - .../lib/factory/typeorm-service.factory.ts | 88 -- .../src/lib/factory/zod-validate.factory.ts | 110 --- .../src/lib/helper/bind-controller.spec.ts | 163 ---- .../src/lib/helper/bind-controller.ts | 131 --- .../src/lib/helper/create-controller.spec.ts | 95 --- .../src/lib/helper/create-controller.ts | 51 -- .../src/lib/helper/error-database/index.ts | 89 -- .../lib/helper/error-database/utils.spec.ts | 28 - .../src/lib/helper/error-database/utils.ts | 14 - .../json-api-nestjs/src/lib/helper/index.ts | 7 - .../src/lib/helper/orm/index.ts | 3 - .../orm/methods/delete-one/delete-one.spec.ts | 66 -- .../orm/methods/delete-one/delete-one.ts | 20 - .../delete-relationship.spec.ts | 187 ----- .../delete-relationship.ts | 33 - .../orm/methods/get-all/get-all.spec.ts | 400 --------- .../lib/helper/orm/methods/get-all/get-all.ts | 263 ------ .../orm/methods/get-one/get-one.spec.ts | 187 ----- .../lib/helper/orm/methods/get-one/get-one.ts | 99 --- .../get-relationship/get-relationship.spec.ts | 129 --- .../get-relationship/get-relationship.ts | 71 -- .../src/lib/helper/orm/methods/index.ts | 52 -- .../orm/methods/patch-one/patch-one.spec.ts | 279 ------- .../helper/orm/methods/patch-one/patch-one.ts | 76 -- .../patch-relationship.spec.ts | 228 ------ .../patch-relationship/patch-relationship.ts | 50 -- .../orm/methods/post-one/post-one.spec.ts | 245 ------ .../helper/orm/methods/post-one/post-one.ts | 41 - .../post-relationship.spec.ts | 203 ----- .../post-relationship/post-relationship.ts | 40 - .../src/lib/helper/orm/orm-helper.spec.ts | 273 ------- .../src/lib/helper/orm/orm-helper.ts | 398 --------- .../src/lib/helper/orm/orm-type-asserts.ts | 27 - .../helper/swagger/filter-operand-model.ts | 84 -- .../src/lib/helper/swagger/index.ts | 54 -- .../lib/helper/swagger/method/delete-one.ts | 48 -- .../swagger/method/delete-relationship.ts | 98 --- .../src/lib/helper/swagger/method/get-all.ts | 232 ------ .../src/lib/helper/swagger/method/get-one.ts | 126 --- .../helper/swagger/method/get-relationship.ts | 67 -- .../src/lib/helper/swagger/method/index.ts | 55 -- .../lib/helper/swagger/method/patch-one.ts | 80 -- .../swagger/method/patch-relationship.ts | 95 --- .../src/lib/helper/swagger/method/post-one.ts | 76 -- .../swagger/method/post-relationship.ts | 96 --- .../src/lib/helper/swagger/utils.ts | 306 ------- .../src/lib/helper/utils.spec.ts | 17 - .../json-api-nestjs/src/lib/helper/utils.ts | 56 -- .../src/lib/helper/zod/index.ts | 1 - .../src/lib/helper/zod/zod-helper.spec.ts | 728 ----------------- .../src/lib/helper/zod/zod-helper.ts | 298 ------- .../index.spec.ts | 46 -- .../index.ts | 11 - .../zod/zod-input-patch-schema/index.ts | 27 - .../relationships.spec.ts | 191 ----- .../zod-input-patch-schema/relationships.ts | 64 -- .../index.spec.ts | 43 - .../index.ts | 23 - .../zod-input-post-schema/attributes.spec.ts | 93 --- .../zod/zod-input-post-schema/attributes.ts | 178 ---- .../zod/zod-input-post-schema/data.spec.ts | 37 - .../helper/zod/zod-input-post-schema/data.ts | 24 - .../zod/zod-input-post-schema/id.spec.ts | 36 - .../helper/zod/zod-input-post-schema/id.ts | 14 - .../helper/zod/zod-input-post-schema/index.ts | 22 - .../relationships.spec.ts | 184 ----- .../zod-input-post-schema/relationships.ts | 67 -- .../zod/zod-input-post-schema/type.spec.ts | 21 - .../helper/zod/zod-input-post-schema/type.ts | 4 - .../zod/zod-input-query-schema/filter.spec.ts | 131 --- .../zod/zod-input-query-schema/filter.ts | 135 --- .../zod/zod-input-query-schema/include.ts | 9 - .../zod/zod-input-query-schema/index.ts | 32 - .../helper/zod/zod-input-query-schema/page.ts | 4 - .../zod/zod-input-query-schema/select.spec.ts | 55 -- .../zod/zod-input-query-schema/select.ts | 63 -- .../helper/zod/zod-input-query-schema/sort.ts | 4 - .../zod/zod-query-schema/filter.spec.ts | 483 ----------- .../lib/helper/zod/zod-query-schema/filter.ts | 278 ------- .../zod/zod-query-schema/include.spec.ts | 40 - .../helper/zod/zod-query-schema/include.ts | 16 - .../lib/helper/zod/zod-query-schema/index.ts | 35 - .../helper/zod/zod-query-schema/page.spec.ts | 64 -- .../lib/helper/zod/zod-query-schema/page.ts | 38 - .../zod/zod-query-schema/select.spec.ts | 141 ---- .../lib/helper/zod/zod-query-schema/select.ts | 78 -- .../helper/zod/zod-query-schema/sort.spec.ts | 197 ----- .../lib/helper/zod/zod-query-schema/sort.ts | 94 --- .../src/lib/helper/zod/zod-utils.spec.ts | 108 --- .../src/lib/helper/zod/zod-utils.ts | 56 -- .../src/lib/json-api-nestjs-common.module.ts | 49 -- .../src/lib/json-api.module.ts | 73 -- .../mixin/controller/json-base.controller.ts | 77 -- .../json-api-nestjs/src/lib/mixin/index.ts | 2 - .../mixin/interceptors/error.interceptors.ts | 116 --- .../src/lib/mixin/interceptors/index.ts | 2 - .../interceptors/log-time.interceptors.ts | 25 - .../src/lib/mixin/module/module.mixin.ts | 74 -- .../check-item-entity.pipe.spec.ts | 68 -- .../check-item-entity.pipe.ts | 37 - .../lib/mixin/pipe/check-item-entity/index.ts | 1 - .../src/lib/mixin/pipe/index.ts | 102 --- .../pipe/parse-relationship-name/index.ts | 1 - .../parse-relationship-name.pipe.spec.ts | 62 -- .../parse-relationship-name.pipe.ts | 30 - .../src/lib/mixin/pipe/patch-input/index.ts | 1 - .../pipe/patch-input/patch-input.pipe.spec.ts | 90 -- .../pipe/patch-input/patch-input.pipe.ts | 32 - .../mixin/pipe/patch-relationship/index.ts | 1 - .../patch-relationship.pipe.spec.ts | 98 --- .../patch-relationship.pipe.ts | 35 - .../src/lib/mixin/pipe/post-input/index.ts | 1 - .../pipe/post-input/post-input.pipe.spec.ts | 89 -- .../mixin/pipe/post-input/post-input.pipe.ts | 31 - .../lib/mixin/pipe/post-relationship/index.ts | 1 - .../post-relationship.pipe.spec.ts | 98 --- .../post-relationship.pipe.ts | 35 - .../pipe/query-check-select-field/index.ts | 1 - .../query-check-select-field.spec.ts | 72 -- .../query-check-select-field.ts | 21 - .../pipe/query-filed-on-include/index.ts | 1 - .../query-filed-in-include.pipe.spec.ts | 120 --- .../query-filed-in-include.pipe.ts | 67 -- .../src/lib/mixin/pipe/query-input/index.ts | 1 - .../pipe/query-input/query-input.pipe.spec.ts | 125 --- .../pipe/query-input/query-input.pipe.ts | 32 - .../src/lib/mixin/pipe/query/index.ts | 1 - .../lib/mixin/pipe/query/query.pipe.spec.ts | 115 --- .../src/lib/mixin/pipe/query/query.pipe.ts | 43 - .../src/lib/mixin/service/index.ts | 2 - .../lib/mixin/service/swagger-bind.service.ts | 93 --- .../service/transform-data.service.spec.ts | 375 --------- .../mixin/service/transform-data.service.ts | 258 ------ .../service/typeorm-utils.service.spec.ts | 768 ------------------ .../mixin/service/typeorm-utils.service.ts | 678 ---------------- .../src/lib/mock-utils/db-for-test | 647 --------------- .../src/lib/mock-utils/entities/addresses.ts | 69 -- .../src/lib/mock-utils/entities/comments.ts | 57 -- .../src/lib/mock-utils/entities/index.ts | 29 - .../src/lib/mock-utils/entities/notes.ts | 44 - .../src/lib/mock-utils/entities/pods.ts | 45 - .../entities/requests-have-pod-locks.ts | 91 --- .../src/lib/mock-utils/entities/requests.ts | 48 -- .../src/lib/mock-utils/entities/roles.ts | 58 -- .../lib/mock-utils/entities/user-groups.ts | 20 - .../src/lib/mock-utils/entities/users.ts | 133 --- .../src/lib/mock-utils/index.ts | 94 --- .../src/lib/mock-utils/utils/index.ts | 2 - .../lib/mock-utils/utils/provider-entities.ts | 69 -- .../src/lib/mock-utils/utils/pull-data.ts | 122 --- .../atomic-operation.module.ts | 71 -- .../atomic-operation/constants/index.ts | 10 - .../atomic-operation/controllers/index.ts | 1 - .../controllers/operation.controller.spec.ts | 213 ----- .../controllers/operation.controller.ts | 104 --- .../factory/async-iterator.ts | 75 -- .../modules/atomic-operation/factory/index.ts | 4 - .../factory/map-controller-entity.ts | 25 - .../factory/map-entity-name-to-entity.ts | 17 - .../factory/zod-input-operation.ts | 30 - .../pipes/input-operation.pipe.spec.ts | 75 -- .../pipes/input-operation.pipe.ts | 31 - .../service/execute.service.spec.ts | 478 ----------- .../service/execute.service.ts | 345 -------- .../service/explorer.service.spec.ts | 97 --- .../service/explorer.service.ts | 108 --- .../modules/atomic-operation/service/index.ts | 3 - .../service/swagger.service.ts | 97 --- .../modules/atomic-operation/types/index.ts | 28 - .../modules/atomic-operation/utils/index.ts | 1 - .../utils/zod/zod-helper.spec.ts | 575 ------------- .../atomic-operation/utils/zod/zod-helper.ts | 225 ----- .../service/entity-props-map.service.spec.ts | 85 -- .../lib/service/entity-props-map.service.ts | 96 --- .../json-api-nestjs/src/lib/service/index.ts | 3 - .../service/transform-input.service.spec.ts | 201 ----- .../lib/service/transform-input.service.ts | 149 ---- .../src/lib/types/binding.types.ts | 46 -- .../src/lib/types/decorator-options.types.ts | 8 - .../src/lib/types/error.types.ts | 18 - .../json-api-nestjs/src/lib/types/index.ts | 8 - .../src/lib/types/module.types.ts | 49 -- .../json-api-nestjs/src/lib/types/operand.ts | 27 - .../json-api-nestjs/src/lib/types/response.ts | 13 - .../src/lib/types/typeorm-service.type.ts | 15 - .../json-api-nestjs/src/lib/types/utils.ts | 64 -- .../json-api-nestjs/tsconfig-mjs.lib.json | 13 - libs/json-api/json-shared-type/.eslintrc.json | 18 - libs/json-api/json-shared-type/README.md | 7 - libs/json-api/json-shared-type/jest.config.ts | 11 - libs/json-api/json-shared-type/project.json | 20 - libs/json-api/json-shared-type/src/index.ts | 1 - .../json-shared-type/src/types/entity-type.ts | 17 - .../json-shared-type/src/types/index.ts | 4 - .../json-shared-type/src/types/query-type.ts | 21 - .../src/types/response-body.ts | 69 -- .../json-shared-type/src/types/utils-type.ts | 3 - libs/json-api/json-shared-type/tsconfig.json | 22 - .../json-shared-type/tsconfig.lib.json | 10 - .../json-shared-type/tsconfig.spec.json | 14 - libs/shared-utils/.eslintrc.json | 18 - libs/shared-utils/README.md | 7 - libs/shared-utils/jest.config.ts | 11 - libs/shared-utils/project.json | 27 - libs/shared-utils/src/index.ts | 2 - libs/shared-utils/src/lib/types/index.ts | 1 - .../src/lib/types/utils-string.type.ts | 16 - libs/shared-utils/src/lib/utils/index.ts | 2 - .../src/lib/utils/object-utils.ts | 14 - .../src/lib/utils/string-utils.spec.ts | 37 - .../src/lib/utils/string-utils.ts | 38 - libs/shared-utils/tsconfig.json | 22 - libs/shared-utils/tsconfig.lib.json | 10 - libs/shared-utils/tsconfig.spec.json | 14 - 230 files changed, 19651 deletions(-) delete mode 100644 libs/json-api/json-api-nestjs/src/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/config/bindings.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/utils.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/json-api.module.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/error.types.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/index.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/module.types.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/operand.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/response.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/utils.ts delete mode 100644 libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json delete mode 100644 libs/json-api/json-shared-type/.eslintrc.json delete mode 100644 libs/json-api/json-shared-type/README.md delete mode 100644 libs/json-api/json-shared-type/jest.config.ts delete mode 100644 libs/json-api/json-shared-type/project.json delete mode 100644 libs/json-api/json-shared-type/src/index.ts delete mode 100644 libs/json-api/json-shared-type/src/types/entity-type.ts delete mode 100644 libs/json-api/json-shared-type/src/types/index.ts delete mode 100644 libs/json-api/json-shared-type/src/types/query-type.ts delete mode 100644 libs/json-api/json-shared-type/src/types/response-body.ts delete mode 100644 libs/json-api/json-shared-type/src/types/utils-type.ts delete mode 100644 libs/json-api/json-shared-type/tsconfig.json delete mode 100644 libs/json-api/json-shared-type/tsconfig.lib.json delete mode 100644 libs/json-api/json-shared-type/tsconfig.spec.json delete mode 100644 libs/shared-utils/.eslintrc.json delete mode 100644 libs/shared-utils/README.md delete mode 100644 libs/shared-utils/jest.config.ts delete mode 100644 libs/shared-utils/project.json delete mode 100644 libs/shared-utils/src/index.ts delete mode 100644 libs/shared-utils/src/lib/types/index.ts delete mode 100644 libs/shared-utils/src/lib/types/utils-string.type.ts delete mode 100644 libs/shared-utils/src/lib/utils/index.ts delete mode 100644 libs/shared-utils/src/lib/utils/object-utils.ts delete mode 100644 libs/shared-utils/src/lib/utils/string-utils.spec.ts delete mode 100644 libs/shared-utils/src/lib/utils/string-utils.ts delete mode 100644 libs/shared-utils/tsconfig.json delete mode 100644 libs/shared-utils/tsconfig.lib.json delete mode 100644 libs/shared-utils/tsconfig.spec.json diff --git a/libs/json-api/json-api-nestjs/src/index.ts b/libs/json-api/json-api-nestjs/src/index.ts deleted file mode 100644 index 1a4e1274..00000000 --- a/libs/json-api/json-api-nestjs/src/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { JsonApiModule } from './lib/json-api.module'; -export { InjectService, JsonApi } from './lib/decorators'; -export { - EntityRelation, - TypeormService as JsonApiService, - ResourceObject, - ResourceObjectRelationships, -} from './lib/types'; -export { JsonBaseController } from './lib/mixin/controller/json-base.controller'; -export { - Query, - PatchData, - PostData, - PostRelationshipData, - PatchRelationshipData, - QueryField, -} from './lib/helper/zod'; -export { excludeMethod } from './lib/config/bindings'; -export { entityForClass } from './lib/helper/utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts b/libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts deleted file mode 100644 index 4c256991..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/config/bindings.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Bindings, excludeMethod } from './bindings'; - -describe('bindings', () => { - it('excludeMethod', () => { - expect(excludeMethod(['patchRelationship'])).toEqual( - Object.keys(Bindings).filter((i) => i !== 'patchRelationship') - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/config/bindings.ts b/libs/json-api/json-api-nestjs/src/lib/config/bindings.ts deleted file mode 100644 index 282f0346..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/config/bindings.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { Body, Param, Query, RequestMethod } from '@nestjs/common'; - -import { BindingsConfig, MethodName } from '../types'; -import { JsonBaseController } from '../mixin'; - -import { PARAMS_RELATION_NAME, PARAMS_RESOURCE_ID } from '../constants'; -import { ObjectTyped } from '../helper'; -import { - queryInputMixin, - queryMixin, - queryFiledInIncludeMixin, - queryCheckSelectFieldMixin, - idPipeMixin, - checkItemEntityPipeMixin, - postInputPipeMixin, - patchInputPipeMixin, - parseRelationshipNamePipeMixin, - postRelationshipPipeMixin, - patchRelationshipPipeMixin, -} from '../mixin/pipe'; - -const Bindings: BindingsConfig = { - getAll: { - method: RequestMethod.GET, - name: 'getAll', - path: '/', - implementation: JsonBaseController.prototype.getAll, - parameters: [ - { - decorator: Query, - mixins: [ - queryInputMixin, - queryMixin, - queryFiledInIncludeMixin, - queryCheckSelectFieldMixin, - ], - }, - ], - }, - getOne: { - method: RequestMethod.GET, - name: 'getOne', - path: `:${PARAMS_RESOURCE_ID}`, - implementation: JsonBaseController.prototype.getOne, - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - decorator: Query, - mixins: [ - queryInputMixin, - queryMixin, - queryFiledInIncludeMixin, - queryCheckSelectFieldMixin, - ], - }, - ], - }, - deleteOne: { - method: RequestMethod.DELETE, - name: 'deleteOne', - path: `:${PARAMS_RESOURCE_ID}`, - implementation: JsonBaseController.prototype.deleteOne, - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - ], - }, - postOne: { - method: RequestMethod.POST, - name: 'postOne', - path: '/', - implementation: JsonBaseController.prototype.postOne, - parameters: [ - { - decorator: Body, - mixins: [postInputPipeMixin], - }, - ], - }, - patchOne: { - method: RequestMethod.PATCH, - name: 'patchOne', - path: `:${PARAMS_RESOURCE_ID}`, - implementation: JsonBaseController.prototype.patchOne, - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - decorator: Body, - mixins: [patchInputPipeMixin], - }, - ], - }, - getRelationship: { - path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, - name: 'getRelationship', - method: RequestMethod.GET, - implementation: JsonBaseController.prototype.getRelationship, - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - property: PARAMS_RELATION_NAME, - decorator: Param, - mixins: [parseRelationshipNamePipeMixin], - }, - ], - }, - postRelationship: { - path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, - name: 'postRelationship', - method: RequestMethod.POST, - implementation: JsonBaseController.prototype['postRelationship'], - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - property: PARAMS_RELATION_NAME, - decorator: Param, - mixins: [parseRelationshipNamePipeMixin], - }, - { - decorator: Body, - mixins: [postRelationshipPipeMixin], - }, - ], - }, - deleteRelationship: { - path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, - name: 'deleteRelationship', - method: RequestMethod.DELETE, - implementation: JsonBaseController.prototype['deleteRelationship'], - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - property: PARAMS_RELATION_NAME, - decorator: Param, - mixins: [parseRelationshipNamePipeMixin], - }, - { - decorator: Body, - mixins: [postRelationshipPipeMixin], - }, - ], - }, - patchRelationship: { - path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, - name: 'patchRelationship', - method: RequestMethod.PATCH, - implementation: JsonBaseController.prototype['patchRelationship'], - parameters: [ - { - property: PARAMS_RESOURCE_ID, - decorator: Param, - mixins: [idPipeMixin, checkItemEntityPipeMixin], - }, - { - property: PARAMS_RELATION_NAME, - decorator: Param, - mixins: [parseRelationshipNamePipeMixin], - }, - { - decorator: Body, - mixins: [patchRelationshipPipeMixin], - }, - ], - }, -}; - -export { Bindings }; - -export function excludeMethod( - names: Array> -): Array { - const tmpObject = names.reduce( - (acum, key) => ((acum[key] = true), acum), - {} as Record, boolean> - ); - return ObjectTyped.keys(Bindings).filter( - (method) => !tmpObject[method] - ) as Array; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts b/libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts deleted file mode 100644 index 627c7834..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/defaults.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ParseIntPipe } from '@nestjs/common'; -import { ConfigParam } from '../types'; - -export const DESC = 'DESC'; -export const ASC = 'ASC'; - -export const SORT_TYPE = [DESC, ASC] as const; - -export const DEFAULT_QUERY_PAGE = 1; -export const DEFAULT_PAGE_SIZE = 20; -export const DEFAULT_CONNECTION_NAME = 'default'; - -export const TYPEORM_SERVICE_PROPS = Symbol('typeormService'); - -export const SUB_QUERY_ALIAS_FOR_PAGINATION = 'subQueryWithLimitOffset'; -export const ALIAS_FOR_PAGINATION = 'aliasForPagination'; - -export const ConfigParamDefault: ConfigParam = { - debug: true, - requiredSelectField: true, - pipeForId: ParseIntPipe, - useSoftDelete: false, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/constants/index.ts deleted file mode 100644 index 40c2a13a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './defaults'; -export * from './reflection'; -export * from './postfix'; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts b/libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts deleted file mode 100644 index 0086dee1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/postfix.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const JSON_API_SERVICE_POSTFIX = 'JsonApiService'; -export const CONFIG_PARAM_POSTFIX = 'JsonApiConfigParam'; -export const JSON_API_CONTROLLER_POSTFIX = 'JsonApiController'; -export const JSON_API_MODULE_POSTFIX = 'JsonApiModule'; -export const TYPEORM_UTILS_SERVICE_POSTFIX = 'TypeormUtilsService'; -export const TYPEORM_MIXIN_SERVICE_POSTFIX = 'JsonApiTypeormService'; -export const TRANSFORM_MIXIN_SERVICE_POSTFIX = 'JsonApiTransformService'; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts b/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts deleted file mode 100644 index 06af80da..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts +++ /dev/null @@ -1,24 +0,0 @@ -export const JSON_API_DECORATOR_ENTITY = Symbol('JSON_API_ENTITY'); -export const JSON_API_DECORATOR_OPTIONS = Symbol('JSON_API_OPTIONS'); -export const GLOBAL_MODULE_OPTIONS_TOKEN = Symbol('GLOBAL_MODULE_OPTIONS'); -export const CURRENT_DATA_SOURCE_TOKEN = Symbol('CURRENT_DATA_SOURCE_TOKEN'); -export const CURRENT_ENTITY_REPOSITORY = Symbol('CURRENT_ENTITY_REPOSITORY'); -export const ZOD_INPUT_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); -export const ZOD_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); -export const ZOD_POST_SCHEMA = Symbol('ZOD_POST_SCHEMA'); -export const SWAGGER_METHOD = Symbol('SWAGGER_METHOD'); -export const ZOD_POST_RELATIONSHIP_SCHEMA = Symbol( - 'ZOD_POST_RELATIONSHIP_SCHEMA' -); -export const ZOD_PATCH_RELATIONSHIP_SCHEMA = Symbol( - 'ZOD_PATCH_RELATIONSHIP_SCHEMA' -); -export const ZOD_PATCH_SCHEMA = Symbol('ZOD_PATCH_SCHEMA'); -export const TYPEORM_SERVICE = Symbol('TYPEORM_SERVICE'); -export const CONTROL_OPTIONS_TOKEN = Symbol('CONTROL_OPTIONS_TOKEN'); - -export const PARAMS_RESOURCE_ID = 'id'; -export const PARAMS_RELATION_ID = 'relId'; -export const PARAMS_RELATION_NAME = 'relName'; - -export const JSON_API_CONFIG = 'JSON_API_CONFIG'; diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/index.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/index.ts deleted file mode 100644 index 2036a50b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './json-api/json-api.decorator'; -export * from './inject-service/inject-service.decorator'; diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts deleted file mode 100644 index cf68012c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - PROPERTY_DEPS_METADATA, - SELF_DECLARED_DEPS_METADATA, -} from '@nestjs/common/constants'; -import { getProviderName } from '../../helper'; -import 'reflect-metadata'; - -import { InjectService } from './inject-service.decorator'; -import { TYPEORM_SERVICE } from '../../constants'; - -describe('InjectServiceDecorator', () => { - it('should save property key', () => { - class SomeClass { - @InjectService() protected property: any; - constructor(@InjectService() protected test: any) {} - } - - const properties = Reflect.getMetadata(PROPERTY_DEPS_METADATA, SomeClass); - const properties1 = Reflect.getMetadata( - SELF_DECLARED_DEPS_METADATA, - SomeClass - ); - expect( - properties.find((item: any) => item.type === TYPEORM_SERVICE) - ).toBeDefined(); - - expect( - properties1.find((item: any) => item.param === TYPEORM_SERVICE) - ).toBeDefined(); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts deleted file mode 100644 index a51b36a9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/inject-service/inject-service.decorator.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Inject } from '@nestjs/common'; - -import { TYPEORM_SERVICE } from '../../constants'; - -export function InjectService(): PropertyDecorator & ParameterDecorator { - return Inject(TYPEORM_SERVICE); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts deleted file mode 100644 index 6f7ffe86..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import 'reflect-metadata'; - -import { - JSON_API_DECORATOR_ENTITY, - JSON_API_DECORATOR_OPTIONS, -} from '../../constants/reflection'; -import { JsonApi } from './json-api.decorator'; -import { DecoratorOptions } from '../../types'; -import { excludeMethod, Bindings } from '../../config/bindings'; - -describe('InjectServiceDecorator', () => { - it('should save entity in class', () => { - const testedEntity = class SomeEntity {}; - - @JsonApi(testedEntity) - class SomeClass {} - - const data = Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, SomeClass); - expect(data).toBe(testedEntity); - }); - - it('should save options in class', () => { - const testedEntity = class SomeEntity {}; - const apiOptions: DecoratorOptions = { - allowMethod: ['getAll', 'deleteRelationship'], - }; - - @JsonApi(testedEntity, apiOptions) - class SomeClass {} - - const data = Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, SomeClass); - expect(data).toEqual(apiOptions); - }); - - it('should save options in class using helpFunction', () => { - const testedEntity = class SomeEntity {}; - const example = ['getAll', 'deleteRelationship']; - const apiOptions: DecoratorOptions = { - allowMethod: excludeMethod(example as any), - }; - - @JsonApi(testedEntity, apiOptions) - class SomeClass {} - - const data: DecoratorOptions = Reflect.getMetadata( - JSON_API_DECORATOR_OPTIONS, - SomeClass - ); - expect(data).toEqual(apiOptions); - expect(data.allowMethod).toEqual( - Object.keys(Bindings).filter((k) => !example.includes(k)) - ); - }); - - it('should save options in class and correctly set overrideRoute', () => { - const testedEntity = class SomeEntity {}; - const apiOptions: DecoratorOptions = { - allowMethod: ['getAll', 'deleteRelationship'], - overrideRoute: '123' - }; - - @JsonApi(testedEntity, apiOptions) - class SomeClass {} - - const data = Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, SomeClass); - expect(data).toEqual(apiOptions); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts deleted file mode 100644 index f0297ded..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/decorators/json-api/json-api.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - JSON_API_DECORATOR_ENTITY, - JSON_API_DECORATOR_OPTIONS, -} from '../../constants'; -import { Entity } from '../../types'; -import { DecoratorOptions } from '../../types'; - -export function JsonApi( - entity: Entity, - options?: DecoratorOptions -): ClassDecorator { - return (target): typeof target => { - Reflect.defineMetadata(JSON_API_DECORATOR_ENTITY, entity, target); - if (options) { - Reflect.defineMetadata(JSON_API_DECORATOR_OPTIONS, options, target); - } - return target; - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts deleted file mode 100644 index 4d284072..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/data-source.factory.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { FactoryProvider } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../constants'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -export function CurrentDataSourceProvider( - connectionName?: string -): FactoryProvider { - return { - provide: CURRENT_DATA_SOURCE_TOKEN, - useFactory: (dataSource: DataSource) => dataSource, - inject: [getDataSourceToken(connectionName)], - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts deleted file mode 100644 index 7903f4e6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/entity-repository.factory.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { FactoryProvider } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; - -import { Entity } from '../types'; -import { - CURRENT_DATA_SOURCE_TOKEN, - CURRENT_ENTITY_REPOSITORY, -} from '../constants'; -import { EntityTarget } from 'typeorm/common/EntityTarget'; - -export function EntityRepositoryFactory( - entity: E -): FactoryProvider> { - return { - provide: CURRENT_ENTITY_REPOSITORY, - useFactory: (dataSource: DataSource) => - dataSource.getRepository(entity as EntityTarget), - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/factory/index.ts deleted file mode 100644 index b529e2bf..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './zod-validate.factory'; -export * from './data-source.factory'; -export * from './typeorm-service.factory'; -export * from './entity-repository.factory'; -export * from './swagger-bind-method'; diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts b/libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts deleted file mode 100644 index 0ab682cc..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/swagger-bind-method.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ValueProvider } from '@nestjs/common'; -import { SwaggerMethod, swaggerMethod } from '../helper/swagger/method'; -import { SWAGGER_METHOD } from '../constants'; - -export const SwaggerBindMethod: ValueProvider = { - provide: SWAGGER_METHOD, - useValue: swaggerMethod, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts deleted file mode 100644 index 28304051..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/typeorm-service.factory.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { FactoryProvider } from '@nestjs/common'; -import { DataSource, Repository } from 'typeorm'; - -import { - ConfigParam, - Entity, - TypeormService, - TypeormServiceObject, -} from '../types'; -import { - CONFIG_PARAM_POSTFIX, - TYPEORM_UTILS_SERVICE_POSTFIX, - CURRENT_DATA_SOURCE_TOKEN, - TYPEORM_SERVICE, - CURRENT_ENTITY_REPOSITORY, - CONTROL_OPTIONS_TOKEN, -} from '../constants'; - -import { - getEntityName, - getProviderName, - MethodsService, - ObjectTyped, -} from '../helper'; -import { TypeormUtilsService } from '../service'; -import { TransformDataService } from '../mixin/service'; - -function guardMethodsServiceName( - methodsService: R, - key: any -): asserts key is keyof R { - if (!(key in methodsService)) - throw new Error(`${key} is not methode of MethodsService`); -} - -export function TypeormServiceFactory( - entity: E -): FactoryProvider> { - const entityName = getEntityName(entity as any); - return { - provide: TYPEORM_SERVICE, - inject: [ - { - token: CURRENT_ENTITY_REPOSITORY, - optional: false, - }, - { - token: CONTROL_OPTIONS_TOKEN, - optional: false, - }, - TypeormUtilsService, - TransformDataService, - ], - useFactory: ( - repository: Repository, - config: ConfigParam, - typeormUtilsService: TypeormUtilsService, - transformDataService: TransformDataService - ) => { - const typeOrmObject: TypeormServiceObject = { - repository, - config, - typeormUtilsService, - transformDataService, - }; - - const bindMethods = ObjectTyped.entries(MethodsService).reduce( - (acum, [key, val]) => ({ - ...acum, - [key]: (val as any).bind(typeOrmObject) as typeof val, - }), - {} as typeof MethodsService - ); - - const target = {} as TypeormService; - return new Proxy>(target, { - get(target: {}, p: string | symbol, receiver: any): any { - try { - guardMethodsServiceName(bindMethods, p); - return bindMethods[p]; - } catch (e) { - return undefined; - } - }, - }); - }, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts b/libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts deleted file mode 100644 index 52952be5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/factory/zod-validate.factory.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Entity, ModuleOptions } from '../types'; -import { FactoryProvider, ValueProvider } from '@nestjs/common'; -import { - ZodInputQuerySchema, - zodInputQuerySchema, - ZodQuerySchema, - zodQuerySchema, - zodInputPostSchema, - ZodInputPostSchema, - zodInputPatchSchema, - ZodInputPatchSchema, - ZodInputPostRelationshipSchema as ZodInputPostRelationshipSchemaType, - ZodInputPatchRelationshipSchema as ZodInputPatchRelationshipSchemaType, - zodInputPostRelationshipSchema, - zodInputPatchRelationshipSchema, -} from '../helper/zod'; -import { - CURRENT_DATA_SOURCE_TOKEN, - ZOD_INPUT_QUERY_SCHEMA, - ZOD_POST_SCHEMA, - ZOD_PATCH_SCHEMA, - ZOD_POST_RELATIONSHIP_SCHEMA, - ZOD_PATCH_RELATIONSHIP_SCHEMA, - ZOD_QUERY_SCHEMA, -} from '../constants'; - -import { EntityTarget } from 'typeorm/common/EntityTarget'; - -export function ZodInputQuerySchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_INPUT_QUERY_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodInputQuerySchema( - dataSource.getRepository(entity as EntityTarget) - ), - }; -} - -export function ZodQuerySchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_QUERY_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodQuerySchema(dataSource.getRepository(entity as EntityTarget)), - }; -} - -export function ZodInputPostSchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_POST_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodInputPostSchema( - dataSource.getRepository(entity as EntityTarget) - ), - }; -} - -export function ZodInputPatchSchema( - entity: E -): FactoryProvider> { - return { - provide: ZOD_PATCH_SCHEMA, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - ], - useFactory: (dataSource: DataSource) => - zodInputPatchSchema( - dataSource.getRepository(entity as EntityTarget) - ), - }; -} - -export const ZodInputPostRelationshipSchema: ValueProvider = - { - provide: ZOD_POST_RELATIONSHIP_SCHEMA, - useValue: zodInputPostRelationshipSchema, - }; - -export const ZodInputPatchRelationshipSchema: ValueProvider = - { - provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, - useValue: zodInputPatchRelationshipSchema, - }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts deleted file mode 100644 index 4c35d4a5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.spec.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { bindController } from './bind-controller'; -import { Users } from '../mock-utils'; -import { DEFAULT_CONNECTION_NAME } from '../constants'; -import { - ParseIntPipe, - Query, - Body, - Param, - PipeTransform, - ArgumentMetadata, -} from '@nestjs/common'; -import { TypeormService } from '../types'; -import { PatchData } from './zod'; -import { JsonApi } from '../decorators'; -import { JsonBaseController } from '../mixin/controller/json-base.controller'; -import { excludeMethod } from '../config/bindings'; -import { - METHOD_METADATA, - PATH_METADATA, - ROUTE_ARGS_METADATA, -} from '@nestjs/common/constants'; -import { ObjectTyped } from './utils'; -import { Bindings } from '../config/bindings'; -import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; - -const mapParams = new Map(); -mapParams.set(Query, RouteParamtypes.QUERY); -mapParams.set(Body, RouteParamtypes.BODY); -mapParams.set(Param, RouteParamtypes.PARAM); - -describe('bindController', () => { - it('Should be all methode', () => { - class Controller {} - const config = { - requiredSelectField: false, - pipeForId: ParseIntPipe, - debug: false, - useSoftDelete: false, - }; - bindController(Controller, Users, DEFAULT_CONNECTION_NAME, config); - - expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ - 'constructor', - 'getAll', - 'getOne', - 'deleteOne', - 'postOne', - 'patchOne', - 'getRelationship', - 'postRelationship', - 'deleteRelationship', - 'patchRelationship', - ]); - - for (const [key, value] of ObjectTyped.entries(Bindings)) { - const descriptor = Reflect.getOwnPropertyDescriptor( - Controller.prototype, - key - ); - if (!descriptor) { - throw new Error('descriptor is empty:' + key); - } - - expect(Reflect.getMetadata(PATH_METADATA, descriptor.value)).toBe( - value.path - ); - expect(Reflect.getMetadata(METHOD_METADATA, descriptor.value)).toBe( - value.method - ); - const paramsMetadata = Reflect.getMetadata( - ROUTE_ARGS_METADATA, - Controller.prototype.constructor, - key - ); - for (const params in value.parameters) { - const tmp = value.parameters[params]; - if (!tmp.decorator) { - expect(paramsMetadata).toEqual(tmp.decorator); - continue; - } - const paramsMetadataItem = - paramsMetadata[`${mapParams.get(tmp.decorator)}:${params}`]; - expect(paramsMetadataItem).not.toEqual(undefined); - expect(paramsMetadataItem.index).toBe(parseInt(params)); - tmp.mixins.forEach((i, k) => { - expect(i(Users, DEFAULT_CONNECTION_NAME, config).name).toEqual( - paramsMetadataItem.pipes[k].name - ); - }); - } - } - }); - - it('Should be without methode: postOne, getRelationship', () => { - @JsonApi(Users, { - allowMethod: excludeMethod(['postOne', 'getRelationship']), - }) - class Controller {} - const config = { - requiredSelectField: false, - pipeForId: ParseIntPipe, - debug: false, - useSoftDelete: false, - }; - bindController(Controller, Users, DEFAULT_CONNECTION_NAME, config); - expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ - 'constructor', - 'getAll', - 'getOne', - 'deleteOne', - 'patchOne', - 'postRelationship', - 'deleteRelationship', - 'patchRelationship', - ]); - }); - - it('Should be use custom pipe', () => { - class SomePipes implements PipeTransform { - transform(value: any, metadata: ArgumentMetadata): any { - return undefined; - } - } - class Controller extends JsonBaseController { - override patchOne( - @Param('id', SomePipes) id: string | number, - @Body(SomePipes) inputData: PatchData - ): ReturnType['patchOne']> { - return super.patchOne(id, inputData); - } - } - const config = { - requiredSelectField: false, - pipeForId: SomePipes, - debug: false, - useSoftDelete: false, - }; - bindController(Controller, Users, DEFAULT_CONNECTION_NAME, config); - - const paramsMetadata = Reflect.getMetadata( - ROUTE_ARGS_METADATA, - Controller.prototype.constructor, - 'patchOne' - ); - expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes[0]).toEqual( - SomePipes - ); - - expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes.length).toBe( - Bindings.patchOne.parameters[0].mixins.length + 1 - ); - expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes.at(-1)).toEqual( - SomePipes - ); - - expect(paramsMetadata[`${mapParams.get(Body)}:1`].pipes.length).toBe( - Bindings.patchOne.parameters[1].mixins.length + 1 - ); - expect(paramsMetadata[`${mapParams.get(Body)}:1`].pipes.at(-1)).toEqual( - SomePipes - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts b/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts deleted file mode 100644 index 28eff2c9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/bind-controller.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { - Body, - Delete, - Get, - HttpCode, - Param, - Patch, - Post, - Query, - RequestMethod, -} from '@nestjs/common'; -import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; -import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; - -import { Bindings } from '../config/bindings'; -import { - ConfigParam, - DecoratorOptions, - Entity, - ExtractNestType, - MethodName, - NestController, -} from '../types'; -import { JSON_API_DECORATOR_OPTIONS } from '../constants'; - -export function bindController( - controller: ExtractNestType, - entity: Entity, - connectionName: string, - config: ConfigParam -): void { - for (const methodName in Bindings) { - const { name, path, parameters, method, implementation } = - Bindings[methodName as MethodName]; - - const decoratorOptions: DecoratorOptions = Reflect.getMetadata( - JSON_API_DECORATOR_OPTIONS, - controller - ); - if (decoratorOptions) { - const { allowMethod = Object.keys(Bindings) } = decoratorOptions; - if (!allowMethod.includes(name)) continue; - } - - if (!Object.prototype.hasOwnProperty.call(controller.prototype, name)) { - // need uniq descriptor for correct work swagger - Reflect.defineProperty(controller.prototype, name, { - value: function ( - ...arg: Parameters - ): ReturnType { - return this.constructor.__proto__.prototype[name].call(this, ...arg); - }, - writable: true, - enumerable: false, - configurable: true, - }); - } - - const descriptor = Reflect.getOwnPropertyDescriptor( - controller.prototype, - name - ); - - if (!descriptor) { - throw new Error( - `Descriptor for "${controller.name}[${name}]" is undefined` - ); - } - - switch (method) { - case RequestMethod.GET: { - Get(path)(controller.prototype, name, descriptor); - break; - } - case RequestMethod.DELETE: { - HttpCode(204)(controller.prototype, name, descriptor); - Delete(path)(controller.prototype, name, descriptor); - break; - } - case RequestMethod.POST: { - Post(path)(controller.prototype, name, descriptor); - break; - } - case RequestMethod.PATCH: { - Patch(path)(controller.prototype, name, descriptor); - break; - } - default: { - throw new Error(`Method '${method}' unsupported`); - } - } - const paramsMetadata = Reflect.getMetadata( - ROUTE_ARGS_METADATA, - controller.prototype.constructor, - name - ); - for (const key in parameters) { - const parameter = parameters[key]; - const { property, decorator, mixins } = parameter; - const resultMixin = mixins.map((mixin) => - mixin(entity, connectionName, config) - ); - - if (paramsMetadata) { - let typeDecorator: RouteParamtypes; - switch (decorator) { - case Query: - typeDecorator = RouteParamtypes.QUERY; - break; - case Param: - typeDecorator = RouteParamtypes.PARAM; - break; - case Body: - typeDecorator = RouteParamtypes.BODY; - } - const tmp = Object.entries(paramsMetadata) - .filter(([k, v]) => k.split(':').at(0) === typeDecorator.toString()) - .reduce( - (acum, [k, v]) => (acum.push(...(v as any).pipes), acum), - [] as any - ); - resultMixin.push(...tmp); - } - decorator(property, ...resultMixin)( - controller.prototype, - name, - parseInt(key, 10) - ); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts deleted file mode 100644 index 2759ce9c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.spec.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - CONTROLLER_WATERMARK, - INTERCEPTORS_METADATA, - PATH_METADATA, - PROPERTY_DEPS_METADATA, -} from '@nestjs/common/constants'; -import { createController } from './create-controller'; -import { Users } from '../mock-utils'; -import { JsonBaseController } from '../mixin/controller/json-base.controller'; -import { - JSON_API_CONTROLLER_POSTFIX, - TYPEORM_SERVICE, - TYPEORM_SERVICE_PROPS, -} from '../constants'; -import { InjectService, JsonApi } from '../decorators'; -import { ErrorInterceptors, LogTimeInterceptors } from '../mixin/interceptors'; - -describe('createController', () => { - it('Should be error', () => { - class TestController {} - expect.assertions(2); - try { - createController(Users, TestController); - } catch (e) { - expect(e).toBeInstanceOf(Error); - expect((e as Error).message).toBe( - 'Controller "TestController" should be inherited of "JsonBaseController"' - ); - } - }); - - it('Should be correct name controller', () => { - class TestController extends JsonBaseController {} - const result = createController(Users); - const result1 = createController(Users, TestController); - expect(result.name).toBe('Users' + JSON_API_CONTROLLER_POSTFIX); - expect(result1.name).toBe('TestController'); - }); - - it('Should be correct path for controller', () => { - const overrideRoute = 'override-route'; - class TestController extends JsonBaseController {} - - @JsonApi(Users, { - overrideRoute, - }) - class TestController2 extends JsonBaseController {} - const result = createController(Users); - const result2 = createController(Users, TestController); - const result3 = createController(Users, TestController2); - - expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result)).toBe(true); - expect(Reflect.getMetadata(PATH_METADATA, result)).toBe('users'); - - expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result2)).toBe(true); - expect(Reflect.getMetadata(PATH_METADATA, result2)).toBe('users'); - - expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result3)).toBe(true); - expect(Reflect.getMetadata(PATH_METADATA, result3)).toBe(overrideRoute); - }); - - it('Check inject typeorm, service', () => { - class TestController extends JsonBaseController { - @InjectService() private tmp: any; - } - - const result = createController(Users); - const result1 = createController(Users, TestController); - - const check = Reflect.getMetadata( - PROPERTY_DEPS_METADATA, - result.prototype.constructor - ); - const check1 = Reflect.getMetadata( - PROPERTY_DEPS_METADATA, - result1.prototype.constructor - ); - - const intecept = Reflect.getMetadata( - INTERCEPTORS_METADATA, - result1.prototype.constructor - ); - expect(intecept).not.toBe(undefined); - expect(intecept[0]).toEqual(LogTimeInterceptors); - expect(intecept[1]).toEqual(ErrorInterceptors); - expect(check[0].key).toBe(TYPEORM_SERVICE_PROPS); - expect(check[0].type).toEqual(TYPEORM_SERVICE); - - expect(check1[0].key).toBe('tmp'); - expect(check1[0].type).toEqual(TYPEORM_SERVICE); - - expect(check1[1].key).toBe(TYPEORM_SERVICE_PROPS); - expect(check1[1].type).toEqual(TYPEORM_SERVICE); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts b/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts deleted file mode 100644 index d4ca8078..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/create-controller.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Controller, Inject, Type, UseInterceptors } from '@nestjs/common'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; - -import { camelToKebab, getProviderName, nameIt } from './utils'; -import { - JSON_API_CONTROLLER_POSTFIX, - JSON_API_DECORATOR_OPTIONS, - TYPEORM_SERVICE, - TYPEORM_SERVICE_PROPS, -} from '../constants'; -import { JsonBaseController } from '../mixin/controller/json-base.controller'; -import { ErrorInterceptors, LogTimeInterceptors } from '../mixin/interceptors'; - -import { DecoratorOptions } from '../types'; - -export function createController( - entity: EntityClassOrSchema, - controller?: Type -): Type { - const controllerClass = - controller || - nameIt( - getProviderName(entity, JSON_API_CONTROLLER_POSTFIX), - JsonBaseController - ); - - const entityName = - entity instanceof Function ? entity.name : entity.options.name; - - if ( - !Object.prototype.isPrototypeOf.call(JsonBaseController, controllerClass) - ) { - throw new Error( - `Controller "${controller?.name}" should be inherited of "JsonBaseController"` - ); - } - - const decoratorOptions: DecoratorOptions = Reflect.getMetadata( - JSON_API_DECORATOR_OPTIONS, - controllerClass - ); - - Controller( - decoratorOptions?.['overrideRoute'] || `${camelToKebab(entityName)}` - )(controllerClass); - - Inject(TYPEORM_SERVICE)(controllerClass.prototype, TYPEORM_SERVICE_PROPS); - UseInterceptors(LogTimeInterceptors)(controllerClass); - UseInterceptors(ErrorInterceptors)(controllerClass); - return controllerClass; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts deleted file mode 100644 index 07ef1b42..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/index.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { EntityMetadata } from 'typeorm'; -import { ValidateQueryError } from '../../types'; - -import { formErrorString } from './utils'; -import { - BadRequestException, - ConflictException, - NotAcceptableException, -} from '@nestjs/common'; - -const fieldNotNullOrDefault = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: formErrorString(entityMetadata, errorText), - path: ['data', 'attributes'], - }; - - return new BadRequestException([error]); -}; - -const duplicateItems = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - errorText = 'Duplicate value'; - if (detail) { - const matches = detail.match(/(?<=\().+?(?=\))/gm); - if (matches) { - errorText = `Duplicate value in the "${matches[0]}"`; - } - } - - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: detail ? formErrorString(entityMetadata, errorText) : errorText, - path: ['data', 'attributes'], - }; - - return new ConflictException([error]); -}; - -const invalidInputSyntax = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: errorText, - path: [], - }; - return new BadRequestException([error]); -}; - -const entityHasRelation = ( - entityMetadata: EntityMetadata, - errorText: string, - detail?: string -) => { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: detail || errorText, - path: ['data', 'attributes'], - }; - return new NotAcceptableException([error]); -}; - -export const MysqlError = { - [1364]: fieldNotNullOrDefault, - [1062]: duplicateItems, - [1525]: invalidInputSyntax, -}; - -export const PostgresError = { - [23502]: fieldNotNullOrDefault, - [23505]: duplicateItems, - ['22P02']: invalidInputSyntax, - [22007]: invalidInputSyntax, - [22003]: invalidInputSyntax, - [23503]: entityHasRelation, -}; - -export type PostgresErrorCode = keyof typeof PostgresError; -export type MysqlErrorCode = keyof typeof MysqlError; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts deleted file mode 100644 index 7fe1e600..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { formErrorString } from './utils'; - -const metadata = { - tableName: 'users', - name: 'Users', - columns: [ - { - databaseName: 'created_at', - propertyName: 'createdAt', - }, - { - databaseName: 'addresses_id', - propertyName: 'addresses', - }, - ], -} as any; - -describe('utils', () => { - it('formErrorString', () => { - const result = formErrorString( - metadata, - `null value in column "${metadata.columns[1].propertyName}" of relation "${metadata.tableName}" violates not-null constraint` - ); - expect(result).toBe( - `null value in column "${metadata.columns[1].propertyName}" of relation "${metadata.name}" violates not-null constraint` - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts deleted file mode 100644 index 06e0f01a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/error-database/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EntityMetadata } from 'typeorm'; - -export const formErrorString = ( - entityMetadata: EntityMetadata, - errorText: string -) => { - for (const column of entityMetadata.columns) { - const result = new RegExp(column.databaseName).test(errorText); - if (!result) continue; - - errorText = errorText.replace(column.databaseName, column.propertyName); - } - return errorText.replace(entityMetadata.tableName, entityMetadata.name); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/index.ts deleted file mode 100644 index de801b8a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './utils'; -export * from './bind-controller'; -export * from './create-controller'; -export * from './zod/zod-helper'; -export * from './swagger'; -export { MethodsService } from './orm'; -export * from './error-database'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts deleted file mode 100644 index b687d4dd..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './orm-helper'; -export * from './orm-type-asserts'; -export * from './methods'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts deleted file mode 100644 index a52e18e5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, -} from '../../../../mock-utils'; -import { CurrentDataSourceProvider } from '../../../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; -import { TypeormService } from '../../../../types'; -import { getRepository, pullUser, Users } from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { Repository } from 'typeorm'; -import { CONTROL_OPTIONS_TOKEN, TYPEORM_SERVICE } from '../../../../constants'; -import { - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { EntityPropsMapService } from '../../../../service'; - -describe('deleteOne', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let user: Users; - let userRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ userRepository } = getRepository(module)); - user = await pullUser(userRepository); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - }); - - it('Should be ok', async () => { - await typeormService.deleteOne(`${user.id}`); - expect(await userRepository.findOneBy({ id: user.id })).toBe(null); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts deleted file mode 100644 index 573a7ed4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-one/delete-one.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Entity, TypeormServiceObject } from '../../../../types'; -import { FindOptionsWhere } from 'typeorm'; - -export async function deleteOne( - this: TypeormServiceObject, - id: number | string -): Promise { - const data = await this.repository.findOne({ - where: { - [this.typeormUtilsService.currentPrimaryColumn.toString()]: id, - } as FindOptionsWhere, - }); - if (!data) return void 0; - - this.config.useSoftDelete - ? await this.repository.softRemove(data) - : await this.repository.remove(data); - - return void 0; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts deleted file mode 100644 index 8c45cdc0..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Repository } from 'typeorm'; - -import { TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { EntityPropsMapService } from '../../../../service'; - -describe('deleteRelationship', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - let typeormUtilsService: TypeormUtilsService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - typeormUtilsService = - module.get>(TypeormUtilsService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', async () => { - const checkUser = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - - const roles = await rolesRepository.find(); - const userGroups = await userGroupRepository.find(); - const users = await userRepository.find(); - - if (!checkUser) { - throw new Error('not found mock'); - } - - const userGroupData = { - type: 'user-groups', - id: userGroups - .find((i) => checkUser.userGroup.id === i.id) - ?.id.toString(), - }; - const rolesData = [ - { - type: 'roles', - id: roles - .find((i) => checkUser.roles.find((a) => a.id === i.id)) - ?.id.toString(), - }, - ]; - - const managerData = { - type: 'users', - id: users.find((i) => checkUser.manager.id === i.id)?.id.toString(), - }; - await typeormService.deleteRelationship(1, 'roles', rolesData as any); - await typeormService.deleteRelationship( - 1, - 'userGroup', - userGroupData as any - ); - await typeormService.deleteRelationship(1, 'manager', managerData as any); - - const checkUserAfterPost = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - if (!checkUserAfterPost) { - throw new Error('not found'); - } - expect(checkUserAfterPost.manager).toBe(null); - expect(checkUserAfterPost.roles.map((i) => i.id.toString()).sort()).toEqual( - checkUser.roles - .map((i) => i.id.toString()) - .filter((i) => !rolesData.map((i) => i.id).includes(i)) - .sort() - ); - expect(checkUserAfterPost.userGroup).toBe(null); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts deleted file mode 100644 index 427ed6cb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/delete-relationship/delete-relationship.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - Entity, - TypeormServiceObject, - EntityRelation, -} from '../../../../types'; - -import { PostRelationshipData } from '../../../zod'; - -export async function deleteRelationship< - E extends Entity, - Rel extends EntityRelation ->( - this: TypeormServiceObject, - id: number | string, - rel: Rel, - input: PostRelationshipData -): Promise { - const idsResult = await this.typeormUtilsService.validateRelationInputData( - rel, - input - ); - const postBuilder = this.repository - .createQueryBuilder() - .relation(rel.toString()) - .of(id); - - if (Array.isArray(idsResult)) { - await postBuilder.remove(idsResult); - } else { - await postBuilder.set(null); - } - return void 0; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts deleted file mode 100644 index 953d8b91..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.spec.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, - DEFAULT_QUERY_PAGE, - DEFAULT_PAGE_SIZE, -} from '../../../../constants'; -import { Entity, TypeormService } from '../../../../types'; -import { Query, QueryField } from '../../../../helper'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { Equal, IsNull, Repository } from 'typeorm'; -import { EntityPropsMapService } from '../../../../service'; - -function getDefaultQuery() { - const filter = { - relation: null, - target: null, - }; - const defaultQuery: Query = { - [QueryField.filter]: filter, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }; - - return defaultQuery; -} - -describe('getAll', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('order', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - - const checkData = await userRepository.find({ - relations: { - addresses: true, - comments: true, - }, - order: { - id: 'DESC', - comments: { - id: 'DESC', - }, - }, - }); - - const query = getDefaultQuery(); - query.include = ['addresses', 'comments']; - query.sort = { - target: { - id: 'DESC', - }, - comments: { - id: 'DESC', - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith(checkData); - }); - - it('include', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - - const checkData = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - addresses: true, - comments: true, - }, - }); - - const query = getDefaultQuery(); - query.include = ['addresses', 'comments']; - query.filter.target = { - id: { - eq: `${checkData?.id}`, - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - it('select', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - - const checkData = await userRepository.findOne({ - select: { - id: true, - isActive: true, - addresses: { - state: true, - id: true, - }, - comments: { - text: true, - id: true, - }, - }, - where: { - id: 1, - }, - relations: { - addresses: true, - comments: true, - }, - }); - - const query = getDefaultQuery(); - query.fields = { - target: ['id', 'isActive'], - addresses: ['state'], - comments: ['text'], - }; - query.include = ['addresses', 'comments']; - query.filter.target = { - id: { - eq: `${checkData?.id}`, - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - describe('filter', () => { - let firstRole: Roles; - let secondRole: Roles; - let addresses: Addresses[]; - let comments: Comments[]; - beforeAll(async () => { - firstRole = (await rolesRepository.findOneBy({ - id: 1, - })) as Roles; - secondRole = (await rolesRepository.findOneBy({ - id: 2, - })) as Roles; - - addresses = await addressesRepository.find(); - comments = await commentsRepository.find(); - }); - - it('Target props with null', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - - const query = getDefaultQuery(); - query.filter.target = { - id: { eq: '1' }, - firstName: {eq: null}, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toHaveBeenCalledTimes(0); - }); - - it('Target props', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.findOne({ - where: { - id: 1, - }, - }); - const query = getDefaultQuery(); - query.filter.target = { - id: { eq: `${checkData?.id}` }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - it('Check relation with the same Entity', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.findOne({ - where: { - id: 1, - comments: { - text: Equal(comments[0].text), - }, - }, - relations: { - comments: true, - }, - }); - const query = getDefaultQuery(); - query.filter.relation = { - comments: { - text: { - eq: comments[0].text, - }, - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - // it('Target relation is null', async () => { - // const query = getDefaultQuery(); - // query.filter.target = { - // comments: { - // eq: 'null', - // }, - // }; - // await typeormService.getAll(query); - // }); - - it('Relation many-to-one', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - manager: true, - }, - }); - - const query = getDefaultQuery(); - query.filter.target = { - id: { - eq: '1', - }, - }; - query.filter.relation = { - manager: { - id: { - eq: '2', - }, - }, - }; - query.include = ['manager']; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - it('Relation one-to-many', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.findOne({ - where: { - id: 1, - addresses: { - state: Equal(addresses[0].state), - }, - }, - relations: { - addresses: true, - }, - }); - const query = getDefaultQuery(); - query.filter.relation = { - addresses: { - state: { - eq: addresses[0].state, - }, - }, - }; - await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); - }); - - it('Relation many-to-many', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const checkData = await userRepository.find({ - where: { - id: 1, - roles: { - name: Equal(firstRole.name), - }, - }, - relations: { - roles: true, - }, - }); - - const query = getDefaultQuery(); - query.include = ['roles']; - query.filter.relation = { - roles: { - name: { - eq: firstRole.name, - }, - }, - }; - const { data } = await typeormService.getAll(query); - expect(spyOnTransformData).not.toBeCalled(); - expect(data).toEqual([]); - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts deleted file mode 100644 index e0e60fb9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-all/get-all.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { Entity, TypeormServiceObject } from '../../../../types'; -import { Query } from '../../../zod'; -import { ObjectTyped } from '../../../utils'; -import { TupleOfEntityRelation } from '../../orm-helper'; -import { - ALIAS_FOR_PAGINATION, - ASC, - DESC, - SUB_QUERY_ALIAS_FOR_PAGINATION, -} from '../../../../constants'; -import { ResourceObject } from '../../../../types/response'; - -type OrderByCondition = Record; - -function getSortObject(params: any, relationName: string) { - return Object.entries(params).reduce((acum, [props, sort]) => { - acum[`${relationName}.${props}`] = `${sort}` === ASC ? ASC : DESC; - return acum; - }, {} as OrderByCondition); -} - -export async function getAll( - this: TypeormServiceObject, - query: Query -): Promise> { - const { fields, filter, include, sort, page } = query; - - let defaultSortObject: OrderByCondition = { - [`${ - this.typeormUtilsService.currentAlias - }.${this.typeormUtilsService.currentPrimaryColumn.toString()}`]: ASC, - }; - - const includeForCountQuery = new Set(); - const selectFields = new Set(); - const includeRel = new Set(); - - const skip = (page.number - 1) * page.size; - - const expressionArrayForTarget = - this.typeormUtilsService.getFilterExpressionForTarget(query); - const expressionArrayForRelation = - this.typeormUtilsService.getFilterExpressionForRelation(query); - const expressionArray = [ - ...expressionArrayForTarget, - ...expressionArrayForRelation, - ]; - - if (sort) { - const { target, ...relation } = sort; - const targetOrder = getSortObject( - target || {}, - this.typeormUtilsService.currentAlias - ); - - const relOrder = Object.entries(relation || {}).reduce( - (acum, [name, order]) => { - return { - ...acum, - ...getSortObject( - order || {}, - this.typeormUtilsService.getAliasForRelation(name) - ), - }; - }, - {} as OrderByCondition - ); - const resultOrder = { - ...targetOrder, - ...relOrder, - }; - if (Object.keys(resultOrder).length > 0) { - defaultSortObject = resultOrder; - } - for (const item of ObjectTyped.keys(relation)) { - includeForCountQuery.add(item.toString()); - } - } - - const queryBuilderForCount = this.repository - .createQueryBuilder(this.typeormUtilsService.currentAlias) - .select( - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - ), - this.typeormUtilsService.currentPrimaryColumn.toString() - ) - .orderBy(defaultSortObject); - - for (const i in expressionArray) { - const { params, alias, selectInclude, expression } = expressionArray[i]; - const expressionTempArray: string[] = []; - if (alias) { - expressionTempArray.push(alias); - } - expressionTempArray.push(expression); - queryBuilderForCount[i === '0' ? 'where' : 'andWhere']( - expressionTempArray.join(' ') - ); - if (params) { - if (Array.isArray(params)) { - for (const { name, val } of params) { - queryBuilderForCount.setParameters({ [name]: val }); - } - } else { - queryBuilderForCount.setParameters({ [params.name]: params.val }); - } - } - if (selectInclude) includeForCountQuery.add(selectInclude); - } - - for (const rel of [...includeForCountQuery]) { - const currentIncludeAlias = - this.typeormUtilsService.getAliasForRelation(rel); - queryBuilderForCount.leftJoin( - this.typeormUtilsService.getAliasPath(rel), - currentIncludeAlias - ); - } - - const count = await queryBuilderForCount.getCount(); - const meta = { - pageNumber: page.number, - totalItems: count, - pageSize: page.size, - }; - - if (count === 0) { - return { - meta, - data: [], - }; - } - - const aliasForIdResultPagination = this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn, - ALIAS_FOR_PAGINATION, - '_' - ); - - const resultIds = await this.repository - .createQueryBuilder(ALIAS_FOR_PAGINATION) - .select( - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn, - ALIAS_FOR_PAGINATION - ), - aliasForIdResultPagination - ) - .innerJoin( - `(${queryBuilderForCount.offset(skip).limit(page.size).getQuery()})`, - SUB_QUERY_ALIAS_FOR_PAGINATION, - `${this.typeormUtilsService.getAliasPath( - queryBuilderForCount.escape( - this.typeormUtilsService.currentPrimaryColumn.toString() - ), - queryBuilderForCount.escape(SUB_QUERY_ALIAS_FOR_PAGINATION) - )} = ${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn, - ALIAS_FOR_PAGINATION - )}` - ) - .setParameters(queryBuilderForCount.getParameters()) - .getRawMany<{ - [K: typeof aliasForIdResultPagination]: number; - }>(); - - const ids = resultIds.map((i) => i[aliasForIdResultPagination]); - if (ids.length === 0) { - return { - meta, - data: [], - }; - } - if (include) { - for (const rel of include) { - includeRel.add(rel); - } - } - - if (fields) { - if (include) { - for (const rel of include) { - const currentIncludeAlias = - this.typeormUtilsService.getAliasForRelation(rel); - const primaryColumnName = - this.typeormUtilsService.getPrimaryColumnForRel(rel); - selectFields.add(`${currentIncludeAlias}.${primaryColumnName}`); - } - } - - const { target, ...other } = fields; - if (target) { - for (const item of target) { - selectFields.add(`${this.typeormUtilsService.currentAlias}.${item}`); - } - } - - for (const [rel, fields] of ObjectTyped.entries(other)) { - const currentIncludeAlias = this.typeormUtilsService.getAliasForRelation( - rel as TupleOfEntityRelation[number] - ); - if (!fields) continue; - for (const field of fields) { - selectFields.add(`${currentIncludeAlias.toString()}.${field}`); - } - } - } - - const resultQuery = this.repository - .createQueryBuilder() - .orderBy(defaultSortObject); - - if (selectFields.size > 0) { - resultQuery.select([...selectFields]); - } - - resultQuery.whereInIds(ids); - for (const expressionItem of expressionArrayForRelation) { - const { selectInclude, alias, paramsForResult, params, expression } = - expressionItem; - if (paramsForResult) { - for (const item of paramsForResult) { - resultQuery.andWhere(item); - } - } else { - resultQuery.andWhere(`${alias} ${expression}`); - } - - if (params) { - if (Array.isArray(params)) { - for (const item of params) { - resultQuery.setParameters({ [item.name]: item.val }); - } - } else { - resultQuery.setParameters({ [params.name]: params.val }); - } - } - if (selectInclude) includeRel.add(selectInclude); - } - - for (const item of [...includeRel]) { - const currentIncludeAlias = - this.typeormUtilsService.getAliasForRelation(item); - if (!currentIncludeAlias) continue; - resultQuery[selectFields.size > 0 ? 'leftJoin' : 'leftJoinAndSelect']( - this.typeormUtilsService.getAliasPath(item), - currentIncludeAlias - ); - } - const resultData = await resultQuery.getMany(); - const { included, data } = - this.transformDataService.transformData(resultData); - return { - meta: { - pageNumber: page.number, - totalItems: count, - pageSize: page.size, - }, - data, - ...(included ? { included } : {}), - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts deleted file mode 100644 index 37dde9b2..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.spec.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Equal, Repository } from 'typeorm'; - -import { Entity, TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - DEFAULT_PAGE_SIZE, - DEFAULT_QUERY_PAGE, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { Query, QueryField } from '../../../zod'; -import { NotFoundException } from '@nestjs/common'; -import { EntityPropsMapService } from '../../../../service'; - -function getDefaultQuery() { - const defaultQuery: Query = { - [QueryField.filter]: { - relation: null, - target: null, - }, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }; - - return defaultQuery; -} - -describe('getOne', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Get one item', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const query = getDefaultQuery(); - const checkData = await userRepository.findOne({ - where: { - id: Equal(1), - }, - relations: { - addresses: true, - comments: true, - }, - }); - query.include = ['addresses', 'comments']; - await typeormService.getOne('1', query); - expect(spyOnTransformData).toBeCalledWith(checkData); - }); - it('Get one item with select', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'transformData' - ); - const query = getDefaultQuery(); - const checkData = await userRepository.findOne({ - select: { - firstName: true, - id: true, - isActive: true, - comments: { - id: true, - text: true, - }, - addresses: { - id: true, - }, - manager: { - id: true, - login: true, - }, - }, - where: { - id: Equal(1), - }, - relations: { - addresses: true, - comments: true, - manager: true, - }, - }); - query.include = ['addresses', 'comments', 'manager']; - query.fields = { - target: ['firstName', 'isActive'], - comments: ['text'], - manager: ['login'], - }; - await typeormService.getOne('1', query); - expect(spyOnTransformData).toBeCalledWith(checkData); - }); - it('Should be error', async () => { - expect.assertions(1); - try { - const query = getDefaultQuery(); - await typeormService.getOne('1000000', query); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts deleted file mode 100644 index 7e96c887..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-one/get-one.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { NotFoundException } from '@nestjs/common'; -import { - Entity, - ResourceObject, - TypeormServiceObject, - ValidateQueryError, -} from '../../../../types'; -import { Query } from '../../../zod'; -import { ObjectTyped } from '../../../utils'; - -export async function getOne( - this: TypeormServiceObject, - id: number | string, - query: Query -): Promise> { - const { include, fields } = query; - const selectFields = new Set(); - const builder = this.repository.createQueryBuilder( - this.typeormUtilsService.currentAlias - ); - - if (fields) { - selectFields.add( - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - ) - ); - - const { target, ...other } = fields; - if (target) { - for (const fieldItem of target) { - selectFields.add(this.typeormUtilsService.getAliasPath(fieldItem)); - } - } - - for (const [rel, fieldRel] of ObjectTyped.entries(other)) { - if (fieldRel) { - for (const itemFieldRel of fieldRel) { - selectFields.add( - this.typeormUtilsService.getAliasPath( - itemFieldRel, - this.typeormUtilsService.getAliasForRelation(rel.toString()) - ) - ); - } - } - } - } - - if (include) { - for (const rel of include) { - const currentIncludeAlias = - this.typeormUtilsService.getAliasForRelation(rel); - - builder[fields ? 'leftJoin' : 'leftJoinAndSelect']( - this.typeormUtilsService.getAliasPath(rel), - currentIncludeAlias - ); - - if (fields) { - selectFields.add( - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.getPrimaryColumnForRel(rel), - currentIncludeAlias - ) - ); - } - } - } - if (selectFields.size > 0) { - builder.select([...selectFields]); - } - const paramsId = 'paramsId'; - const result = await builder - .where( - `${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - )} = :${paramsId}`, - { - [paramsId]: id, - } - ) - .getOne(); - - if (!result) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, - path: ['fields'], - }; - throw new NotFoundException([error]); - } - const { included, data } = this.transformDataService.transformData(result); - return { - meta: {}, - data, - ...(included ? { included } : {}), - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts deleted file mode 100644 index 068cef6b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Repository } from 'typeorm'; - -import { TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; - -import { NotFoundException } from '@nestjs/common'; -import { EntityPropsMapService } from '../../../../service'; - -describe('getRelationship', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'getRelationships' - ); - const id = 1; - const rel = 'roles'; - const check = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - }, - where: { id }, - relations: { - roles: true, - }, - }); - const result = await typeormService.getRelationship(id, rel); - expect(spyOnTransformData).toBeCalledWith(check, rel); - expect(result).toHaveProperty('data'); - }); - it('Should be error', async () => { - expect.assertions(1); - try { - await typeormService.getRelationship('1000000', 'roles'); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts deleted file mode 100644 index 0478c488..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/get-relationship/get-relationship.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - Entity, - TypeormServiceObject, - EntityRelation, - ValidateQueryError, - ResourceObjectRelationships, -} from '../../../../types'; -import { - InternalServerErrorException, - NotFoundException, -} from '@nestjs/common'; - -export async function getRelationship< - E extends Entity, - Rel extends EntityRelation ->( - this: TypeormServiceObject, - id: number | string, - rel: Rel -): Promise> { - const paramsId = 'paramsId'; - const result = await this.repository - .createQueryBuilder() - .select([ - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - ), - this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.getPrimaryColumnForRel(rel.toString()), - this.typeormUtilsService.getAliasForRelation(rel.toString()) - ), - ]) - .where( - ` - ${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - )} = :paramsId - ` - ) - .leftJoin( - this.typeormUtilsService.getAliasPath(rel.toString()), - this.typeormUtilsService.getAliasForRelation(rel.toString()) - ) - .setParameters({ - [paramsId]: id, - }) - .getOne(); - - if (!result) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, - path: ['fields'], - }; - throw new NotFoundException([error]); - } - - const { data } = this.transformDataService.getRelationships(result, rel); - if (data === undefined) { - const error: ValidateQueryError = { - code: 'custom', - message: `transformDataService.getRelationships return undefined`, - path: [], - }; - throw new InternalServerErrorException([error]); - } - return { - meta: {}, - data, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts deleted file mode 100644 index 866c347a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { getAll } from './get-all/get-all'; -import { getOne } from './get-one/get-one'; -import { deleteOne } from './delete-one/delete-one'; -import { postOne } from './post-one/post-one'; -import { patchOne } from './patch-one/patch-one'; -import { getRelationship } from './get-relationship/get-relationship'; -import { postRelationship } from './post-relationship/post-relationship'; -import { deleteRelationship } from './delete-relationship/delete-relationship'; -import { patchRelationship } from './patch-relationship/patch-relationship'; -import { Entity, EntityRelation } from '../../../types'; - -export const MethodsService = { - getAll, - getOne, - deleteOne, - postOne, - patchOne, - getRelationship, - postRelationship, - deleteRelationship, - patchRelationship, -}; - -export type MethodsService = { - getAll: ( - ...arg: Parameters> - ) => ReturnType>; - getOne: ( - ...arg: Parameters> - ) => ReturnType>; - deleteOne: ( - ...arg: Parameters> - ) => ReturnType>; - postOne: ( - ...arg: Parameters> - ) => ReturnType>; - patchOne: ( - ...arg: Parameters> - ) => ReturnType>; - getRelationship: >( - ...arg: Parameters> - ) => ReturnType>; - postRelationship: >( - ...arg: Parameters> - ) => ReturnType>; - deleteRelationship: >( - ...arg: Parameters> - ) => ReturnType>; - patchRelationship: >( - ...arg: Parameters> - ) => ReturnType>; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts deleted file mode 100644 index 38cb8cf3..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.spec.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IBackup, IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; - -import { TypeormService } from '../../../../types'; -import { PatchData, PostData } from '../../../../helper/zod'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { EntityPropsMapService } from '../../../../service'; - -describe('patchOne', () => { - let db: IMemoryDb; - let backaUp: IBackup; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - const firstName = 'firstName test'; - const isActive = false; - const testDate = new Date(); - const login = 'login test'; - - let inputData: PostData; - let newData: PatchData; - - let notes: Notes[]; - let users: Users[]; - let roles: Roles[]; - let userGroup: UserGroups[]; - let addresses: Addresses[]; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - - notes = await notesRepository.find(); - users = await userRepository.find(); - roles = await rolesRepository.find(); - userGroup = await userGroupRepository.find({ - relations: { - users: true, - }, - }); - addresses = await addressesRepository.find(); - - inputData = { - type: 'users', - attributes: { - firstName, - isActive, - testDate, - login, - }, - relationships: { - addresses: { - type: 'addresses', - id: addresses[0].id.toString(), - }, - notes: [ - { - type: 'notes', - id: notes[0].id, - }, - ], - roles: [ - { - type: 'roles', - id: `${roles[0].id}`, - }, - ], - manager: { - type: 'users', - id: `${users[0].id}`, - }, - userGroup: { - type: 'user-group', - id: `${userGroup[0].id}`, - }, - }, - }; - - await typeormService.postOne(inputData); - backaUp = db.backup(); - const changeUser = await userRepository.findOneBy({ - login: inputData.attributes.login as string, - }); - if (!changeUser) { - throw new Error('not found mock data'); - } - newData = { - ...inputData, - id: `${changeUser.id}`, - }; - const newLogin = `${changeUser.login} - newLogin`; - const newIsActive = !changeUser.isActive; - - newData.attributes.login = newLogin; - newData.attributes.isActive = newIsActive; - newData.attributes.testDate = new Date(); - - newData.relationships = { - ...newData.relationships, - manager: { - type: 'users', - id: users[1].id.toString(), - }, - addresses: null, - userGroup: { - type: 'user-group', - id: `${userGroup[1].id}`, - }, - roles: [ - { - type: 'roles', - id: `${roles[1].id}`, - }, - ], - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - backaUp.restore(); - }); - - it('should be ok without relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - })); - - const { relationships, ...withoutRelationships } = newData; - const returnData = await typeormService.patchOne( - withoutRelationships.id, - withoutRelationships - ); - - const result = await userRepository.findOneBy({ - id: parseInt(withoutRelationships.id, 10), - }); - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).not.toHaveProperty('included'); - }); - - it('should be ok with relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - included: {} as any, - })); - - const returnData = await typeormService.patchOne(newData.id, newData); - - const result = await userRepository.findOne({ - where: { - id: parseInt(newData.id, 10), - }, - relations: { - addresses: true, - notes: true, - userGroup: true, - roles: true, - manager: true, - }, - }); - - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).toHaveProperty('included'); - }); - - it('should be ok with relation nulling relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - included: {} as any, - })); - - newData.relationships = { - ...newData.relationships, - userGroup: null, - roles: [], - }; - - const returnData = await typeormService.patchOne(newData.id, newData); - - const result = await userRepository.findOne({ - where: { - id: parseInt(newData.id, 10), - }, - relations: { - addresses: true, - notes: true, - userGroup: true, - roles: true, - manager: true, - }, - }); - - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).toHaveProperty('included'); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts deleted file mode 100644 index 6045e02f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-one/patch-one.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; -import { DeepPartial } from 'typeorm'; -import { - Entity, - ResourceObject, - TypeormServiceObject, - ValidateQueryError, -} from '../../../../types'; -import { PatchData } from '../../../zod'; -import { ObjectTyped } from '../../../utils'; - -export async function patchOne( - this: TypeormServiceObject, - id: number | string, - inputData: PatchData -): Promise> { - const { id: idBody, attributes, relationships } = inputData; - - if (`${id}` !== idBody) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Data 'id' must be equal to url param`, - path: ['data', 'id'], - }; - - throw new UnprocessableEntityException([error]); - } - - const paramsId = 'paramsId'; - const result = await this.repository - .createQueryBuilder() - .where( - `${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - )} = :${paramsId}`, - { - [paramsId]: id, - } - ) - .getOne(); - - if (!result) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, - path: ['data', 'id'], - }; - throw new NotFoundException([error]); - } - - if (attributes) { - const entityTarget = this.repository.manager.create( - this.repository.target, - attributes as DeepPartial - ); - for (const [props, val] of ObjectTyped.entries(entityTarget)) { - result[props] = val; - } - } - - const saveData = await this.typeormUtilsService.saveEntityData( - result, - relationships - ); - - const { data, included } = this.transformDataService.transformData(saveData); - const includeData = included ? { included } : {}; - return { - meta: {}, - data, - ...includeData, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts deleted file mode 100644 index a7c55db0..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.spec.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Repository } from 'typeorm'; - -import { TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { EntityPropsMapService } from '../../../../service'; - -describe('patchRelationship', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - let typeormUtilsService: TypeormUtilsService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - typeormUtilsService = - module.get>(TypeormUtilsService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', async () => { - const checkUser = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - - const roles = await rolesRepository.find(); - const userGroups = await userGroupRepository.find(); - const users = await userRepository.find(); - - if (!checkUser) { - throw new Error('not found mock'); - } - - const userGroupData = { - type: 'user-groups', - id: userGroups - .find((i) => checkUser.userGroup.id !== i.id) - ?.id.toString(), - }; - const rolesData = [ - { - type: 'roles', - id: roles - .find((i) => checkUser.roles.find((a) => a.id !== i.id)) - ?.id.toString(), - }, - ]; - - const managerData = { - type: 'users', - id: users.find((i) => checkUser.manager.id !== i.id)?.id.toString(), - }; - const result = await typeormService.patchRelationship( - 1, - 'roles', - rolesData as any - ); - const result1 = await typeormService.patchRelationship( - 1, - 'userGroup', - userGroupData as any - ); - const result2 = await typeormService.patchRelationship( - 1, - 'manager', - managerData as any - ); - - const checkUserAfterPost = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - if (!checkUserAfterPost) { - throw new Error('not found'); - } - expect(checkUserAfterPost.manager.id.toString()).toBe(managerData.id); - expect(checkUserAfterPost.roles.map((i) => i.id.toString())).toEqual( - rolesData.map((i) => i.id) - ); - expect(checkUserAfterPost.userGroup.id.toString()).toBe(userGroupData.id); - - await typeormService.patchRelationship(1, 'roles', []); - await typeormService.patchRelationship(1, 'manager', null); - const checkUserAfterPatch = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - if (!checkUserAfterPatch) { - throw new Error('not found'); - } - - expect(checkUserAfterPatch.manager).toBe(null); - expect(checkUserAfterPatch.roles).toEqual([]); - expect(result.data.map((i) => i.id)).toEqual( - checkUserAfterPost.roles.map((i) => i.id.toString()) - ); - expect(result2.data?.id).toEqual(checkUserAfterPost.manager.id.toString()); - expect(result1.data?.id).toEqual( - checkUserAfterPost.userGroup.id.toString() - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts deleted file mode 100644 index 13f77e8a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/patch-relationship/patch-relationship.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { - Entity, - TypeormServiceObject, - EntityRelation, - ResourceObjectRelationships, -} from '../../../../types'; - -import { PatchRelationshipData } from '../../../zod'; -import { getRelationship } from '../get-relationship/get-relationship'; - -export async function patchRelationship< - E extends Entity, - Rel extends EntityRelation ->( - this: TypeormServiceObject, - id: number | string, - rel: Rel, - input: PatchRelationshipData -): Promise> { - const idsResult = await this.typeormUtilsService.validateRelationInputData( - rel, - input - ); - - const patchBuilder = this.repository - .createQueryBuilder() - .relation(rel.toString()) - .of(id); - - if (Array.isArray(idsResult)) { - const data = await getRelationship.call< - TypeormServiceObject, - [number | string, Rel], - Promise> - >(this, id, rel); - const idsToDelete = Array.isArray(data.data) - ? data.data.map((i) => i.id) - : []; - - await patchBuilder.addAndRemove(idsResult, idsToDelete); - } else { - await patchBuilder.set(idsResult); - } - - return getRelationship.call< - TypeormServiceObject, - [number | string, Rel], - Promise> - >(this, id, rel); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts deleted file mode 100644 index 084e375f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.spec.ts +++ /dev/null @@ -1,245 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IBackup, IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - Pods, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; - -import { TypeormService } from '../../../../types'; -import { PostData } from '../../../../helper/zod'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { EntityPropsMapService } from '../../../../service'; - -describe('postOne', () => { - let db: IMemoryDb; - let backaUp: IBackup; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - let podsRepository: Repository; - - let typeormServicePods: TypeormService; - let transformDataServicePods: TransformDataService; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - const firstName = 'firstName test'; - const isActive = false; - const testDate = new Date(); - const login = 'login test'; - - let inputData: PostData; - - let notes: Notes[]; - let users: Users[]; - let roles: Roles[]; - let userGroup: UserGroups[]; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - - const modulePods: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Pods), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Pods), - EntityPropsMapService, - ], - }).compile(); - - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - podsRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - backaUp = db.backup(); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - - typeormServicePods = modulePods.get>(TYPEORM_SERVICE); - transformDataServicePods = - modulePods.get>(TransformDataService); - - notes = await notesRepository.find(); - users = await userRepository.find(); - roles = await rolesRepository.find(); - userGroup = await userGroupRepository.find(); - - inputData = { - type: 'users', - attributes: { - firstName, - isActive, - testDate, - login, - }, - relationships: { - notes: [ - { - type: 'notes', - id: notes[0].id, - }, - ], - roles: [ - { - type: 'roles', - id: `${roles[0].id}`, - }, - ], - manager: { - type: 'users', - id: `${users[0].id}`, - }, - userGroup: { - type: 'user-group', - id: `${userGroup[0].id}`, - }, - }, - }; - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - backaUp.restore(); - }); - - it('should be ok without relation and with id', async () => { - const spyOnTransformData = jest - .spyOn(transformDataServicePods, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - })); - const { relationships, ...other } = inputData; - const id = '5'; - const returnData = await typeormServicePods.postOne({ - id, - type: 'pods', - attributes: { - name: 'test', - }, - }); - const result = await podsRepository.findOneBy({ - id, - }); - - expect(spyOnTransformData).toBeCalledWith({ - ...result, - id, - }); - expect(returnData).not.toHaveProperty('included'); - }); - - it('should be ok without relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - })); - const { relationships, ...other } = inputData; - const returnData = await typeormService.postOne(other); - const result = await userRepository.findOneBy({ - login, - }); - - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).not.toHaveProperty('included'); - }); - - it('should be ok with relation', async () => { - const spyOnTransformData = jest - .spyOn(transformDataService, 'transformData') - .mockImplementationOnce(() => ({ - data: {} as any, - included: {} as any, - })); - const returnData = await typeormService.postOne(inputData); - const result = await userRepository.findOne({ - where: { - login, - }, - relations: { - notes: true, - userGroup: true, - roles: true, - manager: true, - }, - }); - - expect(spyOnTransformData).toBeCalledWith(result); - expect(returnData).toHaveProperty('included'); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts deleted file mode 100644 index 97ba0a7c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-one/post-one.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { DeepPartial } from 'typeorm'; -import { - Entity, - ResourceObject, - TypeormServiceObject, -} from '../../../../types'; -import { PostData } from '../../../zod'; - -export async function postOne( - this: TypeormServiceObject, - inputData: PostData -): Promise> { - const { attributes, relationships, id } = inputData; - - const idObject = id - ? { [this.typeormUtilsService.currentPrimaryColumn.toString()]: id } - : {}; - - const attributesObject = { - ...attributes, - ...idObject, - } as DeepPartial; - - const entityTarget = this.repository.manager.create( - this.repository.target, - attributesObject - ); - - const saveData = await this.typeormUtilsService.saveEntityData( - entityTarget, - relationships - ); - - const { data, included } = this.transformDataService.transformData(saveData); - const includeData = included ? { included } : {}; - return { - meta: {}, - data, - ...includeData, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts deleted file mode 100644 index 966c7b0b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.spec.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { Repository } from 'typeorm'; - -import { TypeormService } from '../../../../types'; -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../../mock-utils'; -import { - TransformDataService, - TypeormUtilsService, -} from '../../../../mixin/service'; -import { EntityPropsMapService } from '../../../../service'; - -import { - CONTROL_OPTIONS_TOKEN, - DEFAULT_CONNECTION_NAME, - TYPEORM_SERVICE, -} from '../../../../constants'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, - TypeormServiceFactory, -} from '../../../../factory'; - -describe('postRelationship', () => { - let db: IMemoryDb; - let typeormService: TypeormService; - let transformDataService: TransformDataService; - let typeormUtilsService: TypeormUtilsService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - EntityRepositoryFactory(Users), - TypeormUtilsService, - TransformDataService, - TypeormServiceFactory(Users), - EntityPropsMapService, - ], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - typeormService = module.get>(TYPEORM_SERVICE); - transformDataService = - module.get>(TransformDataService); - typeormUtilsService = - module.get>(TypeormUtilsService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', async () => { - const checkUser = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - - const roles = await rolesRepository.find(); - const userGroups = await userGroupRepository.find(); - const users = await userRepository.find(); - - if (!checkUser) { - throw new Error('not found mock'); - } - - const userGroupData = { - type: 'user-groups', - id: userGroups - .find((i) => checkUser.userGroup.id !== i.id) - ?.id.toString(), - }; - const rolesData = [ - { - type: 'roles', - id: roles - .find((i) => checkUser.roles.find((a) => a.id !== i.id)) - ?.id.toString(), - }, - ]; - - const managerData = { - type: 'users', - id: users.find((i) => checkUser.manager.id !== i.id)?.id.toString(), - }; - const result = await typeormService.postRelationship( - 1, - 'roles', - rolesData as any - ); - const result1 = await typeormService.postRelationship( - 1, - 'userGroup', - userGroupData as any - ); - - const result2 = await typeormService.postRelationship( - 1, - 'manager', - managerData as any - ); - - const checkUserAfterPost = await userRepository.findOne({ - select: { - id: true, - roles: { - id: true, - }, - userGroup: { - id: true, - }, - manager: { - id: true, - }, - }, - where: { id: 1 }, - relations: { - roles: true, - manager: true, - userGroup: true, - }, - }); - if (!checkUserAfterPost) { - throw new Error('not found'); - } - - expect(checkUserAfterPost.manager.id.toString()).toBe(managerData.id); - expect(checkUserAfterPost.roles.map((i) => i.id.toString())).toEqual([ - ...checkUser.roles.map((i) => i.id.toString()), - ...rolesData.map((i) => i.id), - ]); - expect(checkUserAfterPost.userGroup.id.toString()).toBe(userGroupData.id); - - expect(result.data.map((i) => i.id)).toEqual( - checkUserAfterPost.roles.map((i) => i.id.toString()) - ); - expect(result2.data?.id).toEqual(checkUserAfterPost.manager.id.toString()); - expect(result1.data?.id).toEqual( - checkUserAfterPost.userGroup.id.toString() - ); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts deleted file mode 100644 index 11652e12..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/methods/post-relationship/post-relationship.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - Entity, - TypeormServiceObject, - EntityRelation, - ResourceObjectRelationships, -} from '../../../../types'; - -import { PostRelationshipData } from '../../../zod'; -import { getRelationship } from '../get-relationship/get-relationship'; - -export async function postRelationship< - E extends Entity, - Rel extends EntityRelation ->( - this: TypeormServiceObject, - id: number | string, - rel: Rel, - input: PostRelationshipData -): Promise> { - const idsResult = await this.typeormUtilsService.validateRelationInputData( - rel, - input - ); - const postBuilder = this.repository - .createQueryBuilder() - .relation(rel.toString()) - .of(id); - - if (Array.isArray(idsResult)) { - await postBuilder.add(idsResult); - } else { - await postBuilder.set(idsResult); - } - - return getRelationship.call< - TypeormServiceObject, - [number | string, Rel], - Promise> - >(this, id, rel); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts deleted file mode 100644 index 525a4cab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.spec.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { - mockDBTestModule, - createAndPullSchemaBase, - pullUser, - pullAllData, - providerEntities, - getRepository, - Users, - Addresses, - Notes, - Comments, - Roles, - UserGroups, -} from '../../mock-utils'; -import { EntityProps, EntityRelation, TypeOfArray } from '../../types'; -import { - getField, - getPropsTreeForRepository, - fromRelationTreeToArrayName, - getArrayFields, - PropsArray, - getArrayPropsForEntity, - ArrayPropsForEntity, - getFieldWithType, - getRelationTypeArray, - getRelationTypeName, - getRelationTypePrimaryColumn, - TypeField, - getTypePrimaryColumn, - getPrimaryColumnsForRelation, - getIsArrayRelation, - getTypeForAllProps, - getPropsFromDb, -} from './orm-helper'; -import { ObjectTyped } from '../utils'; - -describe('zod-helper', () => { - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - let db: IMemoryDb; - let user: Users; - let userWithRelation: Users; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [...providerEntities(getDataSourceToken())], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - - user = await pullUser(userRepository); - userWithRelation = await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - }); - - it('getField', async () => { - const { relations, field } = getField(userRepository); - const userFieldProps = Object.getOwnPropertyNames( - user - ) as EntityProps[]; - const hasUserFieldInResultField = userFieldProps.some( - (field) => !field.includes(field) - ); - - const hasResultInUserField = field.some( - (field) => !userFieldProps.includes(field) - ); - - const userRelationProps: EntityRelation[] = ( - Object.getOwnPropertyNames(userWithRelation) as (EntityProps & - EntityRelation)[] - ).filter((props) => !userFieldProps.includes(props)); - - const hasUserRelationInResultField = userRelationProps.some( - (field) => !relations.includes(field) - ); - - const hasResultInUserRelation = relations.some( - (field) => !userRelationProps.includes(field) - ); - - expect(hasUserFieldInResultField).toEqual(false); - expect(hasResultInUserField).toEqual(false); - - expect(hasUserRelationInResultField).toEqual(false); - expect(hasResultInUserRelation).toEqual(false); - }); - - it('getPropsTreeForRepository', () => { - const relationField = getPropsTreeForRepository(userRepository); - const userFieldProps = Object.getOwnPropertyNames( - user - ) as EntityProps[]; - const userRelationProps: EntityRelation[] = ( - Object.getOwnPropertyNames(userWithRelation) as (EntityProps & - EntityRelation)[] - ).filter((props) => !userFieldProps.includes(props)); - - const hasUserRelationInResultField = userRelationProps.some( - (field) => !Object.keys(relationField).includes(field) - ); - const hasResultInUserRelation = ObjectTyped.keys(relationField).some( - (field) => !userRelationProps.includes(field) - ); - expect(hasUserRelationInResultField).toEqual(false); - expect(hasResultInUserRelation).toEqual(false); - - for (const [relationName, fieldsRelation] of ObjectTyped.entries( - relationField - )) { - const check = fieldsRelation.some((field) => { - const targetItem = userWithRelation[relationName]; - const target = Array.isArray(targetItem) ? targetItem[0] : targetItem; - // @ts-ignore - return !ObjectTyped.keys(target).includes(field); - }); - expect(check).toEqual(false); - } - }); - - it('fromRelationTreeToArrayName', () => { - const { relations, field } = getField(userRepository); - - const relationField = getPropsTreeForRepository(userRepository); - const checkArray = fromRelationTreeToArrayName(relationField); - - for (const key of relations) { - let resultKey = - key === 'manager' ? 'Users' : key === 'userGroup' ? 'UserGroups' : key; - - const relationsRepo = - userRepository.metadata.connection.getRepository< - TypeOfArray - >(resultKey); - const { field: relationsFields } = getField(relationsRepo); - const textField = relationsFields.map((r) => `${key}.${r}`); - const check = textField.some((i) => !checkArray.includes(i as any)); - expect(check).toEqual(false); - } - }); - - it('getArrayFields', () => { - const result = getArrayFields(addressesRepository); - expect(result).toEqual({ - arrayField: true, - } as PropsArray); - }); - - it('getArrayPropsForEntity', () => { - const result = getArrayPropsForEntity(userRepository); - const check: ArrayPropsForEntity = { - target: { - testReal: true, - testArrayNull: true, - }, - manager: { - testReal: true, - testArrayNull: true, - }, - comments: {}, - notes: {}, - userGroup: {}, - roles: {}, - addresses: { - arrayField: true, - }, - }; - expect(result).toEqual(check); - }); - - it('getFieldWithType', () => { - const result = getFieldWithType(addressesRepository); - expect(result.arrayField).toBe('array'); - expect(result.state).toBe('string'); - expect(result.id).toBe('number'); - expect(result.createdAt).toBe('date'); - const result2 = getFieldWithType(userRepository); - - expect(result2.isActive).toBe('boolean'); - }); - - it('getRelationType', () => { - const result = getRelationTypeArray(userRepository); - expect(result.roles).toBe(true); - expect(result.comments).toBe(true); - expect(result.manager).toBe(false); - expect(result.addresses).toBe(false); - expect(result.userGroup).toBe(false); - expect(result.notes).toBe(true); - }); - - it('getRelationTypeName', () => { - const result = getRelationTypeName(userRepository); - expect(result.roles).toBe('Roles'); - expect(result.comments).toBe('Comments'); - expect(result.manager).toBe('Users'); - expect(result.addresses).toBe('Addresses'); - expect(result.userGroup).toBe('UserGroups'); - expect(result.notes).toBe('Notes'); - }); - - it('getRelationTypePrimaryColumn', () => { - const result = getRelationTypePrimaryColumn(userRepository); - expect(result.roles).toBe(TypeField.number); - expect(result.comments).toBe(TypeField.number); - expect(result.manager).toBe(TypeField.number); - expect(result.addresses).toBe(TypeField.number); - expect(result.userGroup).toBe(TypeField.number); - expect(result.notes).toBe(TypeField.string); - }); - - it('getTypePrimaryColumn', () => { - expect(getTypePrimaryColumn(userRepository)).toBe(TypeField.number); - expect(getTypePrimaryColumn(notesRepository)).toBe(TypeField.string); - }); - - it('getPrimaryColumnsForRelation', () => { - const result = getPrimaryColumnsForRelation(userRepository); - expect(result.roles).toBe('id'); - expect(result.manager).toBe('id'); - }); - - it('geTisArrayRelation', () => { - const result = getIsArrayRelation(userRepository); - expect(result).toEqual({ - addresses: false, - manager: false, - roles: true, - comments: true, - notes: true, - userGroup: false, - }); - }); - - it('getTypeForAllProps', () => { - const result = getTypeForAllProps(userRepository); - expect(result.manager.id).toBe(TypeField.number); - expect(result.testDate).toBe(TypeField.date); - expect(result.comments.id).toBe(TypeField.number); - expect(result.notes.id).toBe(TypeField.string); - }); - - it('getPropsFromDb', () => { - const result = getPropsFromDb(userRepository); - expect(result['testReal']).toEqual({ - type: 'real', - isArray: true, - isNullable: false, - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts deleted file mode 100644 index d73f0835..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-helper.ts +++ /dev/null @@ -1,398 +0,0 @@ -import { Repository } from 'typeorm'; -import { Type } from '@nestjs/common'; -import { - CastProps, - Concat, - Entity, - EntityProps, - EntityPropsArray, - EntityRelation, - IsArray, - TypeCast, - TypeOfArray, - UnionToTuple, - ValueOf, -} from '../../types'; -import { getEntityName, ObjectTyped } from '../utils'; -import { guardKeyForPropertyTarget } from './orm-type-asserts'; -import { ColumnType } from 'typeorm/driver/types/ColumnTypes'; - -export enum PropsNameResultField { - field = 'field', - relations = 'relations', -} - -export type ResultGetField = { - [PropsNameResultField.field]: TupleOfEntityProps; - [PropsNameResultField.relations]: TupleOfEntityRelation; -}; - -export type TupleOfEntityProps< - E, - Props = UnionToTuple> -> = Props extends readonly [string, ...string[]] ? Props : never; -export type TupleOfEntityRelation< - E, - Props = UnionToTuple> -> = Props extends readonly [string, ...string[]] ? Props : never; - -export type RelationTree = { - [K in keyof RelationType]: TypeOfArray extends Entity - ? ResultGetField>['field'] - : never; -}; - -export type ConcatFieldWithRelation< - R extends string, - T extends readonly string[] -> = ValueOf<{ - [K in T[number]]: Concat; -}>; - -export type ConcatRelationUnion< - E extends Entity, - R = RelationTree -> = ValueOf<{ - [K in keyof R]: ConcatFieldWithRelation< - TypeCast, - TypeCast - >; -}>; - -export type ConcatRelation = TypeCast< - UnionToTuple>, - [string, ...string[]] ->; - -type RelationType = { - [K in EntityRelation]: Type>>; -}; - -export enum TypeField { - array = 'array', - date = 'date', - number = 'number', - boolean = 'boolean', - string = 'string', - object = 'object', -} - -export type TypeForId = Extract; - -export type FieldWithType = { - [K in EntityProps]: IsArray extends true - ? TypeField.array - : E[K] extends Date - ? TypeField.date - : E[K] extends number - ? TypeField.number - : E[K] extends boolean - ? TypeField.boolean - : E[K] extends object - ? TypeField.object - : TypeField.string; -}; - -export type RelationPropsType = { - [K in EntityRelation]: E[K] extends unknown[] ? true : false; -}; - -export type RelationPropsTypeName = { - [K in EntityRelation]: string; -}; - -export type RelationPrimaryColumnType = { - [K in EntityRelation]: TypeForId; -}; - -export const getRelationTypeArray = ( - repository: Repository -): RelationPropsType => { - const { relations } = getField(repository); - - const entity = repository.target as any; - const result = {} as any; - for (const item of relations) { - result[item] = - Reflect.getMetadata('design:type', entity['prototype'], item) === Array; - } - return result; -}; - -export const getTypePrimaryColumn = ( - repository: Repository -): TypeForId => { - const target = repository.target as any; - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - return Reflect.getMetadata( - 'design:type', - target['prototype'], - primaryColumn - ) === Number - ? TypeField.number - : TypeField.string; -}; - -export const getRelationTypePrimaryColumn = ( - repository: Repository -): RelationPrimaryColumnType => { - return repository.metadata.relations.reduce((acum, i) => { - const target = i.inverseEntityMetadata.target as any; - const primaryColumn = - i.inverseEntityMetadata.primaryColumns[0].propertyName; - acum[i.propertyName] = - Reflect.getMetadata('design:type', target['prototype'], primaryColumn) === - Number - ? TypeField.number - : TypeField.string; - return acum; - }, {} as Record) as RelationPrimaryColumnType; -}; - -export const getPrimaryColumnsForRelation = ( - repository: Repository -): RelationPropsTypeName => { - return repository.metadata.relations.reduce((acum, i) => { - const target = i.inverseEntityMetadata.target as any; - acum[i.propertyName] = - i.inverseEntityMetadata.primaryColumns[0].propertyName; - return acum; - }, {} as Record) as RelationPropsTypeName; -}; - -export const getRelationTypeName = ( - repository: Repository -): RelationPropsTypeName => { - return repository.metadata.relations.reduce((acum, i) => { - acum[i.propertyName] = getEntityName(i.inverseEntityMetadata.target); - return acum; - }, {} as Record) as RelationPropsTypeName; -}; - -export const getFieldWithType = ( - repository: Repository -): FieldWithType => { - const { field } = getField(repository); - - const entity = repository.target as any; - const result = {} as any; - for (const item of field) { - let typeProps: TypeField = TypeField.string; - switch (Reflect.getMetadata('design:type', entity['prototype'], item)) { - case Array: - typeProps = TypeField.array; - break; - case Date: - typeProps = TypeField.date; - break; - case Number: - typeProps = TypeField.number; - break; - case Boolean: - typeProps = TypeField.boolean; - break; - case Object: - typeProps = TypeField.object; - break; - default: - typeProps = TypeField.string; - } - result[item] = typeProps; - } - - return result; -}; - -export const getField = ( - repository: Repository -): ResultGetField => { - const relations = repository.metadata.relations.map((i) => { - return i.propertyName; - }); - - const field = repository.metadata.columns - .filter((i) => !relations.includes(i.propertyName)) - .map((r) => r.propertyName); - - return { - field, - relations, - } as unknown as ResultGetField; -}; - -export type PropsArray = { [K in EntityPropsArray]: true }; - -export const getArrayFields = ( - repository: Repository -): PropsArray => { - const relations = repository.metadata.relations.map((i) => { - return i.propertyName; - }); - - return repository.metadata.columns - .filter((i) => !relations.includes(i.propertyName)) - .reduce((acum, metaData) => { - if (metaData.isArray) { - acum[metaData.propertyName] = true; - } - return acum; - }, {} as Record) as PropsArray; -}; - -export type ArrayPropsForEntity = { - target: PropsArray; -} & { - [K in ResultGetField['relations'][number]]: PropsArray< - TypeOfArray> - >; -}; - -export type PropertyTarget< - E extends Entity, - For extends PropsNameResultField -> = { - [K in ResultGetField[For][number]]: K extends keyof E - ? TypeOfArray - : never; -}; - -export const getArrayPropsForEntity = ( - repository: Repository -): ArrayPropsForEntity => { - const connection = repository.metadata.connection; - - const relationsTargets = repository.metadata.relations.reduce( - (acum, i) => ({ - ...acum, - [i.propertyName]: i.type, - }), - {} as Record - ) as PropertyTarget; - - const { relations } = getField(repository); - const relationsArrayFields = relations.reduce( - (acum, item) => { - guardKeyForPropertyTarget(relationsTargets, item); - const target = relationsTargets[item] as TypeCast< - TypeOfArray>, - Entity - >; - - const repository = connection.getRepository( - target as Function - ) as Repository; - - acum[item] = getArrayFields(repository) as PropsArray< - TypeOfArray> - >; - - return acum; - }, - {} as { - [K in ResultGetField['relations'][number]]: PropsArray< - TypeOfArray> - >; - } - ); - - return { - target: getArrayFields(repository), - ...relationsArrayFields, - }; -}; - -export const getPropsTreeForRepository = ( - repository: Repository -): RelationTree => { - const dataSource = repository.metadata.connection; - - const relationType = repository.metadata.relations.reduce((acum, i) => { - acum[i.propertyName] = i.inverseEntityMetadata.target; - return acum; - }, {} as Record) as unknown as RelationType; - - return ObjectTyped.entries(relationType).reduce( - (acum, [key, value]) => ({ - ...acum, - ...{ [key]: getField(dataSource.getRepository(value))['field'] }, - }), - {} as RelationTree - ); -}; - -export const getIsArrayRelation = ( - repository: Repository -): { - [K in EntityRelation]: E[K] extends unknown[] ? true : false; -} => { - return repository.metadata.relations.reduce((acum, i) => { - switch (i.relationType) { - case 'one-to-many': - case 'many-to-many': - acum[i.propertyName] = true; - break; - default: - acum[i.propertyName] = false; - } - return acum; - }, {} as any) as any; -}; - -export const fromRelationTreeToArrayName = ( - relationTree: RelationTree -): ConcatRelation => { - return ObjectTyped.entries(relationTree).reduce((acum, [name, filed]) => { - acum.push(...filed.map((i) => `${name.toLocaleString()}.${i}`)); - return acum; - }, [] as string[]) as unknown as ConcatRelation; -}; - -export type AllFieldWithTpe = FieldWithType & { - [K in EntityRelation]: E[K] extends (infer U extends Entity)[] - ? FieldWithType - : E[K] extends Entity - ? FieldWithType - : never; -}; - -export const getTypeForAllProps = ( - repository: Repository -): AllFieldWithTpe => { - const targetField = getFieldWithType(repository); - - const relationField = repository.metadata.relations.reduce((acum, item) => { - acum[item.propertyName] = getFieldWithType( - repository.manager.getRepository(item.inverseEntityMetadata.target) - ); - return acum; - }, {} as any); - - return { - ...targetField, - ...relationField, - }; -}; - -export type PropsFieldItem = { - type: ColumnType; - isArray: boolean; - isNullable: boolean; -}; - -export type PropsForField = { - [K in EntityProps]: PropsFieldItem; -}; - -export const getPropsFromDb = ( - repository: Repository -): PropsForField => { - return repository.metadata.columns.reduce((acum, i) => { - acum[i.propertyName as EntityProps] = { - type: i.type, - isArray: i.isArray, - isNullable: i.isNullable, - }; - return acum; - }, {} as PropsForField); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts b/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts deleted file mode 100644 index 779f339d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/orm/orm-type-asserts.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Entity } from '../../types'; -import { - PropsNameResultField, - PropertyTarget, -} from './orm-helper'; - -export function guardKeyForPropertyTarget< - E extends Entity, - For extends PropsNameResultField, - R extends PropertyTarget ->(relationsTargets: R, key: any): asserts key is keyof R { - if (!(key in relationsTargets)) throw new Error('Type guard error'); -} - -export function guardIsKeyOfObject( - object: R, - key: string | number | symbol -): asserts key is keyof R { - if (typeof object === 'object' && object !== null && key in object) - return void 0; - - throw new Error('Type guard error'); -} - -export function guardIsArray(input: T|Array): input is Array{ - return Array.isArray(input) -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts deleted file mode 100644 index bba7eaba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/filter-operand-model.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { FilterOperand as FilterOperandType } from '../../types'; - -const title = 'is equal to the conditional of query'; - -export const OperandsMapTitle = { - [FilterOperandType.in]: `${title} "WHERE 'attribute_name' IN ('value1', 'value2')"`, - [FilterOperandType.nin]: `${title} "WHERE 'attribute_name' NOT IN ('value1', 'value1')"`, - [FilterOperandType.eq]: `${title} "WHERE 'attribute_name' = 'value1'`, - [FilterOperandType.ne]: `${title} "WHERE 'attribute_name' <> 'value1'`, - [FilterOperandType.gt]: `${title} "WHERE 'attribute_name' > 'value1'`, - [FilterOperandType.gte]: `${title} "WHERE 'attribute_name' >= 'value1'`, - [FilterOperandType.like]: `${title} "WHERE 'attribute_name' ILIKE %value1%`, - [FilterOperandType.lt]: `${title} "WHERE 'attribute_name' < 'value1'`, - [FilterOperandType.lte]: `${title} "WHERE 'attribute_name' <= 'value1'`, - [FilterOperandType.regexp]: `${title} "WHERE 'attribute_name' ~* value1`, - [FilterOperandType.some]: `${title} "WHERE 'attribute_name' && [value1]`, -}; - -export class FilterOperand { - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.in], - required: false, - type: 'array', - items: { - type: 'string', - }, - }) - [FilterOperandType.in]!: string[]; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.nin], - required: false, - type: 'array', - items: { - type: 'string', - }, - }) - [FilterOperandType.nin]!: string[]; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.eq], - required: false, - }) - [FilterOperandType.eq]!: string; - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.ne], - required: false, - }) - [FilterOperandType.ne]!: string; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.gte], - required: false, - }) - [FilterOperandType.gte]!: string; - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.gt], - required: false, - }) - [FilterOperandType.gt]!: string; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.lt], - required: false, - }) - [FilterOperandType.lt]!: string; - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.lte], - required: false, - }) - [FilterOperandType.lte]!: string; - - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.regexp], - required: false, - }) - [FilterOperandType.regexp]!: string; - @ApiProperty({ - title: OperandsMapTitle[FilterOperandType.some], - required: false, - }) - [FilterOperandType.some]!: string; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts deleted file mode 100644 index e0bff43c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -// import { Type } from '@nestjs/common'; -// import { DECORATORS } from '@nestjs/swagger/dist/constants'; -// import { ApiTags, ApiExtraModels } from '@nestjs/swagger'; -// // import { camelToKebab, ObjectTyped } from '../utils'; -// // import { FilterOperand } from './filter-operand-model'; -// // import { JSON_API_DECORATOR_OPTIONS } from '../../constants'; -// // import { DecoratorOptions, Entity, MethodName, ConfigParam } from '../../types'; -// import { Bindings } from '../../config/bindings'; -// import { swaggerMethod } from './method'; -// // import { createApiModels } from './utils'; -// -// import { Entity, ConfigParam } from '../../types'; -// import { ObjectTyped } from '../utils'; -// -// export function setSwaggerDecorator( -// controller: Type, -// entity: Entity, -// config: ConfigParam -// ) { -// // const apiTag = Reflect.getMetadata(DECORATORS.API_TAGS, controller); -// // if (!apiTag) { -// // const entityName = -// // entity instanceof Function ? entity.name : entity.options.name; -// // -// // ApiTags(config?.['overrideRoute'] || `${camelToKebab(entityName)}`)( -// // controller -// // ); -// // } -// // ApiExtraModels(FilterOperand)(controller); -// // ApiExtraModels(createApiModels(entity))(controller); -// // -// // const decoratorOptions: DecoratorOptions = Reflect.getMetadata( -// // JSON_API_DECORATOR_OPTIONS, -// // controller -// // ); -// // -// for (const method of ObjectTyped.keys(Bindings)) { -// // if (decoratorOptions) { -// // const { allowMethod = Object.keys(Bindings) } = decoratorOptions; -// // -// // if (!allowMethod.includes(method as MethodName)) { -// // continue; -// // } -// // } -// // -// if (method in swaggerMethod) { -// swaggerMethod[method](controller, entity, Bindings[method], config); -// } -// } -// } - -export * from './method'; -export * from './filter-operand-model'; -export { createApiModels } from './utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts deleted file mode 100644 index 0d0b7f73..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-one.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { ApiParam, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { ParseIntPipe, Type } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { Binding, Entity, ConfigParam } from '../../../types'; -import { errorSchema } from '../utils'; - -export function deleteOne( - controller: Type, - repository: Repository, - binding: Binding<'deleteOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiOperation({ - summary: `Delete item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: `Item of resource "${entityName}" not found`, - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 204, - description: `Item of resource "${entityName}" has been deleted`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong query parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts deleted file mode 100644 index f8568432..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/delete-relationship.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { ParseIntPipe, Type } from '@nestjs/common'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { createZodDto } from '@anatine/zod-nestjs'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { Repository } from 'typeorm'; - -import { Binding, ConfigParam, Entity } from '../../../types'; -import { errorSchema } from '../utils'; -import { getField } from '../../orm'; -import { zodInputPatchRelationshipSchema, zodInputPostSchema } from '../../zod'; - -export function deleteRelationship( - controller: Type, - repository: Repository, - binding: Binding<'deleteRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor( - controller.constructor.prototype, - binding.name - ); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}DeleteRelationship`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Delete list of relation for resource "${entityName}"`, - operationId: `${controller.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for delete "${entityName}" item`, - schema: generateSchema(zodInputPatchRelationshipSchema) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 204, - description: `Item/s of relation for "${entityName}" has been deleted`, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts deleted file mode 100644 index c5506eba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-all.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { Type } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../constants'; -import { Binding, Entity, ConfigParam } from '../../../types'; -import { ObjectTyped } from '../../utils'; -import { errorSchema, jsonSchemaResponse } from '../utils'; - -import { - fromRelationTreeToArrayName, - getField, - getPrimaryColumnsForRelation, - getPropsTreeForRepository, -} from '../../orm'; - -export function getAll( - controller: Type, - repository: Repository, - binding: Binding<'getAll'>, - config: ConfigParam -): void { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations, field } = getField(repository); - const propsTree = getPropsTreeForRepository(repository); - const relationTree = fromRelationTreeToArrayName(propsTree); - const relationPrimaryColum = getPrimaryColumnsForRelation(repository); - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - ApiOperation({ - summary: `Get list items of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'fields', - required: false, - style: 'deepObject', - schema: { - type: 'object', - }, - examples: { - allField: { - summary: 'Select all field', - description: 'Select field for target and relation', - value: { - target: field.join(','), - ...ObjectTyped.entries(propsTree).reduce((acum, [name, props]) => { - acum[name.toString()] = props.join(','); - return acum; - }, {} as Record), - }, - }, - selectOnlyIdsTarget: { - summary: 'Select ids for target', - description: 'Select ids for target', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - }, - }, - selectOnlyIds: { - summary: 'Select ids', - description: 'Select ids', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - ...ObjectTyped.entries(relationPrimaryColum).reduce( - (acum, [name, props]) => { - acum[name.toString()] = props; - return acum; - }, - {} as Record - ), - }, - }, - }, - description: `Object of field for select field from "${entityName}" resource`, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'filter', - required: false, - style: 'deepObject', - schema: { - type: 'object', - }, - examples: { - simpleExample: { - summary: 'Several conditional', - description: 'Get if relation is not null', - value: { - [field[0]]: { - in: '1,2,3', - }, - [field[1]]: { - lt: '1', - }, - [relationTree[0]]: { - eq: 'test', - }, - }, - }, - relationNull: { - summary: 'Get if relation is null', - description: 'Get if relation is null', - value: { - [relations[0]]: { - eq: null, - }, - }, - }, - relationNotNull: { - summary: 'Get if relation is not null', - description: 'Get if relation is not null', - value: { - [relations[0]]: { - ne: null, - }, - }, - }, - getRelationByConditional: { - summary: 'Get if relation field is', - description: 'Get if relation field is', - value: { - [relationTree[0]]: { - eq: 'test', - }, - }, - }, - }, - description: `Object of filter for select items from "${entityName}" resource`, - })(controller, binding.name, descriptor); - ApiQuery({ - name: 'include', - required: false, - enum: relations, - style: 'simple', - isArray: true, - description: `"${entityName}" resource item has been extended with existing relations`, - examples: { - withInclude: { - summary: 'Add all relation', - description: 'Add all realtion', - value: relations, - }, - without: { - summary: 'Without relation', - description: 'Without all realtion', - value: [], - }, - }, - })(controller, binding.name, descriptor); - ApiQuery({ - name: 'sort', - type: 'string', - required: false, - description: `Params for sorting of "${entityName}"`, - examples: { - sortAsc: { - summary: 'Sort field by ASC', - description: 'Sort field by ASC', - value: field[1], - }, - sortDesc: { - summary: 'Sort field by DESC', - description: 'Sort field by DESC', - value: `-${field[1]}`, - }, - sortAscRelation: { - summary: 'Sort field relation by ASC', - description: 'Sort field relation by ASC', - value: relationTree[2], - }, - sortDescRelation: { - summary: 'Sort field relation by DESC', - description: 'Sort field relation by DESC', - value: `-${relationTree[2]}`, - }, - sortSeveral: { - summary: 'Sort several field relation', - description: 'Sort several field relation', - value: `${field[1]},-${relationTree[2]},${relationTree[1]},-${field[0]}`, - }, - }, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'page', - style: 'deepObject', - required: false, - schema: { - type: 'object', - properties: { - number: { - type: 'integer', - minimum: 1, - example: DEFAULT_QUERY_PAGE, - }, - size: { - type: 'integer', - minimum: 1, - example: DEFAULT_PAGE_SIZE, - maximum: 500, - }, - }, - additionalProperties: false, - }, - description: `"${entityName}" resource has been limit and offset with this params.`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: `Item of resource "${entityName}" not found`, - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong query parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - description: 'Resource list received successfully', - schema: jsonSchemaResponse(repository, true), - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts deleted file mode 100644 index 765238fe..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-one.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { Repository } from 'typeorm'; - -import { Binding, Entity, ConfigParam } from '../../../types'; -import { jsonSchemaResponse, errorSchema } from '../utils'; - -import { - getField, - getPropsTreeForRepository, - getPrimaryColumnsForRelation, -} from '../../orm'; -import { ObjectTyped } from '../../utils'; - -export function getOne( - controller: Type, - repository: Repository, - binding: Binding<'getOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - if (!descriptor) - throw new Error( - `Descriptor for entity controller ${entityName}:${binding.name} is empty` - ); - - const { relations, field } = getField(repository); - const propsTree = getPropsTreeForRepository(repository); - const relationPrimaryColum = getPrimaryColumnsForRelation(repository); - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - ApiOperation({ - summary: `Get one item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiQuery({ - name: 'fields', - required: false, - style: 'deepObject', - schema: { - type: 'object', - }, - examples: { - allField: { - summary: 'Select all field', - description: 'Select field for target and relation', - value: { - target: field.join(','), - ...ObjectTyped.entries(propsTree).reduce((acum, [name, props]) => { - acum[name.toString()] = props.join(','); - return acum; - }, {} as Record), - }, - }, - selectOnlyIdsTarget: { - summary: 'Select ids for target', - description: 'Select ids for target', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - }, - }, - selectOnlyIds: { - summary: 'Select ids', - description: 'Select ids', - value: { - target: field.filter((i) => i === primaryColumn).join(','), - ...ObjectTyped.entries(relationPrimaryColum).reduce( - (acum, [name, props]) => { - acum[name.toString()] = props; - return acum; - }, - {} as Record - ), - }, - }, - }, - description: `Object of field for select field from "${entityName}" resource`, - })(controller, binding.name, descriptor); - ApiQuery({ - name: 'include', - required: false, - enum: relations, - style: 'simple', - isArray: true, - description: `"${entityName}" resource item has been extended with existing relations`, - examples: { - withInclude: { - summary: 'Add all relation', - description: 'Add all realtion', - value: relations, - }, - without: { - summary: 'Without relation', - description: 'Without all realtion', - value: [], - }, - }, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: `Item of resource "${entityName}" not found`, - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong query parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - description: 'Resource one item received successfully', - schema: jsonSchemaResponse(repository), - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts deleted file mode 100644 index 31d817f4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/get-relationship.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; -import { Repository } from 'typeorm'; - -import { Binding, ConfigParam, Entity } from '../../../types'; -import { errorSchema, schemaTypeForRelation } from '../utils'; -import { getField } from '../../orm'; - -export function getRelationship( - controller: Type, - repository: Repository, - binding: Binding<'getRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - ApiOperation({ - summary: `Get list of relation for resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - schema: schemaTypeForRelation, - description: `Item/s of relation for "${entityName}" has been created`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts deleted file mode 100644 index 232f6eff..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/index.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { getAll } from './get-all'; -import { getOne } from './get-one'; -import { deleteOne } from './delete-one'; -import { postOne } from './post-one'; -import { patchOne } from './patch-one'; -import { getRelationship } from './get-relationship'; -import { deleteRelationship } from './delete-relationship'; -import { postRelationship } from './post-relationship'; -import { patchRelationship } from './patch-relationship'; -import { MethodName } from '../../../types'; - -export type SwaggerMethod = { - [Key in MethodName]?: any; -}; - -export const swaggerMethod: SwaggerMethod = { - getAll, - getOne, - deleteOne, - postOne, - patchOne, - getRelationship, - deleteRelationship, - postRelationship, - patchRelationship, -}; - -export const errorSchema = { - type: 'object', - properties: { - status: { - type: 'number', - }, - errors: { - type: 'array', - items: { - type: 'object', - properties: { - detail: { - type: 'string', - }, - source: { - type: 'object', - properties: { - parameter: { - type: 'string', - }, - }, - }, - }, - required: ['detail'], - }, - }, - }, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts deleted file mode 100644 index a4abf4aa..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-one.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { ParseIntPipe, Type } from '@nestjs/common'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { Repository } from 'typeorm'; - -import { jsonSchemaResponse, errorSchema } from '../utils'; - -import { zodInputPatchSchema, zodInputPostSchema } from '../../zod'; -import { Binding, ConfigParam, Entity } from '../../../types'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function patchOne( - controller: Type, - repository: Repository, - binding: Binding<'patchOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PatchOne`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Update item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for update "${entityName}" item`, - schema: generateSchema(zodInputPatchSchema(repository)) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - description: `Item of resource "${entityName}" has been updated`, - schema: jsonSchemaResponse(repository), - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong body parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Unprocessable data', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts deleted file mode 100644 index b87b2cfa..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/patch-relationship.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { Repository } from 'typeorm'; -import { Binding, ConfigParam, Entity } from '../../../types'; -import { schemaTypeForRelation, errorSchema } from '../utils'; -import { zodInputPatchRelationshipSchema, zodInputPostSchema } from '../../zod'; -import { getField } from '../../orm'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function patchRelationship( - controller: Type, - repository: Repository, - binding: Binding<'patchRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PatchRelationship`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Update list of relation for resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller.prototype, binding.name, descriptor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller.prototype, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for update "${entityName}" item`, - schema: generateSchema(zodInputPatchRelationshipSchema) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 200, - schema: schemaTypeForRelation, - description: `Item/s of relation for "${entityName}" has been updated`, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller.prototype, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller.prototype, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts deleted file mode 100644 index 608e0fae..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-one.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { - ApiOperation, - ApiResponse, - ApiBody, - ApiExtraModels, -} from '@nestjs/swagger'; - -import { Binding, ConfigParam, Entity } from '../../../types'; -import { jsonSchemaResponse } from '../utils'; - -import { errorSchema } from '../utils'; -import { Type } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { generateSchema, extendApi } from '@anatine/zod-openapi'; - -import { zodInputPostSchema } from '../../zod'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function postOne( - controller: Type, - repository: Repository, - binding: Binding<'postOne'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PostOne`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiOperation({ - summary: `Create item of resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for new "${entityName}" item`, - schema: generateSchema(zodInputPostSchema(repository)) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 201, - description: `Item of resource "${entityName}" has been created`, - schema: jsonSchemaResponse(repository), - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong body parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Unprocessable data', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts deleted file mode 100644 index 6cdeb525..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/method/post-relationship.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ParseIntPipe, Type } from '@nestjs/common'; -import { - ApiBody, - ApiExtraModels, - ApiOperation, - ApiParam, - ApiResponse, -} from '@nestjs/swagger'; -import { extendApi, generateSchema } from '@anatine/zod-openapi'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { Repository } from 'typeorm'; - -import { getField } from '../../orm'; -import { zodInputPostRelationshipSchema, zodInputPostSchema } from '../../zod'; -import { Binding, ConfigParam, Entity } from '../../../types'; -import { schemaTypeForRelation, errorSchema } from '../utils'; -import { createZodDto } from '@anatine/zod-nestjs'; - -export function postRelationship( - controller: Type, - repository: Repository, - binding: Binding<'postRelationship'>, - config: ConfigParam -) { - const entityName = repository.metadata.name; - - const descriptor = Reflect.getOwnPropertyDescriptor(controller, binding.name); - - if (!descriptor) - throw new Error(`Descriptor for entity controller ${entityName} is empty`); - - const { relations } = getField(repository); - - const classBodySchemaDto = createZodDto( - extendApi(zodInputPostSchema(repository)) - ); - Object.defineProperty(classBodySchemaDto, 'name', { - value: `${entityName}PostRelationship`, - }); - ApiExtraModels(classBodySchemaDto)(controller.constructor); - - ApiParam({ - name: 'id', - required: true, - type: config.pipeForId === ParseIntPipe ? 'integer' : 'string', - description: `ID of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiParam({ - name: 'relName', - required: true, - type: 'string', - enum: relations, - description: `Relation name of resource "${entityName}"`, - })(controller, binding.name, descriptor); - - ApiBody({ - description: `Json api schema for update "${entityName}" item`, - schema: generateSchema(zodInputPostRelationshipSchema) as - | SchemaObject - | ReferenceObject, - required: true, - })(controller, binding.name, descriptor); - - ApiOperation({ - summary: `Create list of relation for resource "${entityName}"`, - operationId: `${controller.constructor.name}_${binding.name}`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 200, - schema: schemaTypeForRelation, - description: `Item/s of relation for "${entityName}" has been created`, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 400, - description: 'Wrong url parameters', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 422, - description: 'Incorrect type for relation', - schema: errorSchema, - })(controller, binding.name, descriptor); - - ApiResponse({ - status: 404, - description: 'Resource not found ', - schema: errorSchema, - })(controller, binding.name, descriptor); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts deleted file mode 100644 index 58b91728..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/swagger/utils.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; -import { Type } from '@nestjs/common'; -import { ApiProperty } from '@nestjs/swagger'; -import { DeepPartial } from 'typeorm/common/DeepPartial'; -import { Repository } from 'typeorm'; - -import { - getField, - getFieldWithType, - TypeField, - getIsArrayRelation, - getRelationTypeName, -} from '../orm'; -import { camelToKebab, nameIt, ObjectTyped } from '../utils'; -import { Entity, EntityRelation } from '../../types'; - -export const errorSchema = { - type: 'object', - properties: { - statusCode: { - type: 'number', - }, - error: { - type: 'string', - }, - message: { - type: 'array', - items: { - type: 'object', - properties: { - code: { - type: 'string', - }, - message: { - type: 'string', - }, - path: { - type: 'array', - items: { - type: 'string', - }, - }, - keys: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - required: ['code', 'message', 'path'], - }, - }, - }, -}; - -export function jsonSchemaResponse( - repository: Repository, - array = false -) { - const { relations } = getField(repository); - const fieldTypes = getFieldWithType(repository); - const arrayField = getIsArrayRelation(repository); - const relationTypeName = getRelationTypeName(repository); - const primaryColumn = repository.metadata.primaryColumns[0].propertyName; - - const dataType = { - type: 'object', - properties: { - type: { - type: 'string', - enum: [camelToKebab(repository.metadata.name)], - }, - id: { - type: 'string', - }, - attributes: { - type: 'object', - properties: ObjectTyped.entries(fieldTypes) - .filter(([name]) => name !== primaryColumn) - .reduce((acum, [name, type]) => { - switch (type) { - case TypeField.array: - acum[name.toString()] = { - type: 'array', - items: { - type: 'string', - }, - }; - break; - case TypeField.date: - acum[name.toString()] = { - format: 'date-time', - type: 'string', - }; - break; - case TypeField.number: - acum[name.toString()] = { - type: 'integer', - }; - break; - case TypeField.boolean: - acum[name.toString()] = { - type: 'boolean', - }; - break; - default: - acum[name.toString()] = { - type: 'string', - }; - } - return acum; - }, {} as Record), - }, - relationships: { - type: 'object', - properties: relations.reduce((acum, name) => { - const dataItem = { - type: 'object', - properties: { - type: { - type: 'string', - enum: [ - camelToKebab(relationTypeName[name as EntityRelation]), - ], - }, - id: { - type: 'string', - }, - }, - required: ['type', 'id'], - }; - const dataArray = { - type: 'array', - items: dataItem, - }; - acum[name.toString()] = { - type: 'object', - properties: { - links: { - type: 'object', - properties: { - self: { - type: 'string', - }, - }, - required: ['self'], - }, - data: arrayField[name as EntityRelation] - ? dataArray - : dataItem, - }, - required: ['links'], - }; - return acum; - }, {} as Record), - }, - links: { - type: 'object', - properties: { - self: { - type: 'string', - }, - }, - required: ['self'], - }, - }, - }; - const dataTypeArra = { - type: 'array', - items: dataType, - }; - return { - type: 'object', - properties: { - meta: { - type: 'object', - }, - data: array ? dataTypeArra : dataType, - includes: { - type: 'array', - items: { - type: 'object', - properties: { - type: { - type: 'string', - }, - id: { - type: 'string', - }, - attributes: { - type: 'object', - }, - relationships: { - type: 'object', - properties: { - relationName: { - properties: { - links: { - type: 'object', - properties: { - self: { - type: 'string', - }, - }, - required: ['self'], - }, - }, - required: ['links'], - }, - }, - }, - links: { - type: 'object', - properties: { - self: { - type: 'string', - }, - }, - required: ['self'], - }, - }, - required: ['type', 'id', 'attributes'], - }, - }, - }, - required: ['meta', 'data'], - }; -} - -export function createApiModels( - repository: Repository -): Type { - const propsType = getFieldWithType(repository); - const relationTypeName = getRelationTypeName(repository); - const relationArray = getIsArrayRelation(repository); - - const result = repository.create({ - ...propsType, - ...ObjectTyped.entries(relationTypeName).reduce((acum, [name, value]) => { - acum[name.toString()] = relationArray[name] ? [value] : value; - return acum; - }, {} as any), - } as DeepPartial); - - const newEntity = nameIt(repository.metadata.name, class {}) as Type; - for (const [name, value] of Object.entries(result)) { - let currentType: any; - let isArray = false; - if (name in propsType) { - switch (value) { - case TypeField.array: - currentType = String; - isArray = true; - break; - case TypeField.date: - currentType = Date; - break; - case TypeField.number: - currentType = Number; - break; - case TypeField.boolean: - currentType = Boolean; - break; - default: - currentType = String; - } - } else { - currentType = relationTypeName[name as EntityRelation]; - isArray = Array.isArray(value); - } - ApiProperty({ - required: false, - isArray: isArray, - type: () => currentType, - })(newEntity.prototype, name); - } - - return newEntity; -} - -const dataType = { - type: 'object', - properties: { - type: { - type: 'string', - }, - id: { - type: 'string', - }, - }, -}; -export const schemaTypeForRelation = { - type: 'object', - properties: { - data: { - oneOf: [ - dataType, - { type: 'null' }, - { - type: 'array', - items: dataType, - }, - ], - }, - }, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts deleted file mode 100644 index fd1d708e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/utils.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { getEntityName, nameIt } from './'; - -describe('Test utils', () => { - it('getEntityName', () => { - expect(getEntityName('Entity')).toBe('Entity'); - expect(getEntityName(class EntityClass {})).toBe('EntityClass'); - class EntityClassInst {} - const tmp = new EntityClassInst(); - expect(getEntityName(tmp as any)).toBe('EntityClassInst'); - }); - - it('nameIt', () => { - const newNameClass = 'newNameClass'; - const newClass = nameIt(newNameClass, class {}); - expect(getEntityName(newClass)).toBe(newNameClass); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/utils.ts deleted file mode 100644 index 1ac82eee..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { EntityTarget } from 'typeorm/common/EntityTarget'; -import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; -import { Type } from '@nestjs/common/interfaces'; -import { JSON_API_DECORATOR_ENTITY } from '../constants'; - -import { Entity } from '../types'; - -import { upperFirstLetter } from 'shared-utils'; - -export { - camelToKebab, - snakeToCamel, - kebabToCamel, - upperFirstLetter, - isString, - ObjectTyped, -} from 'shared-utils'; - -export const nameIt = ( - name: string, - cls: new (...rest: unknown[]) => Record -) => - ({ - [name]: class extends cls { - constructor(...arg: unknown[]) { - super(...arg); - } - }, - }[name]); - -export const getEntityName = ( - entity: EntityTarget -): string => { - if (typeof entity === 'string') { - return entity; - } - - if ('name' in entity) { - return entity['name']; - } - - if ('constructor' in entity && 'name' in entity.constructor) { - return entity['constructor']['name']; - } - - return `${entity}`; -}; - -export function getProviderName(entity: EntityTarget, name: string) { - const entityName = getEntityName(entity); - return `${upperFirstLetter(entityName)}${name}`; -} - -export function entityForClass(type: Type): Entity { - return Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, type); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts deleted file mode 100644 index b70e311c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './zod-helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts deleted file mode 100644 index 3221b9bc..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.spec.ts +++ /dev/null @@ -1,728 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; -import { IMemoryDb } from 'pg-mem'; -import { z, ZodError } from 'zod'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - pullUser, - Roles, - UserGroups, - Users, -} from '../../mock-utils'; -import { - QueryField, - zodInputQuerySchema, - ZodInputQuerySchema, - zodQuerySchema, - ZodQuerySchema, - zodInputPostSchema, - ZodInputPostSchema, - ZodInputPatchSchema, - zodInputPatchSchema, - zodInputPostRelationshipSchema, - zodInputPatchRelationshipSchema, -} from './zod-helper'; -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../constants'; - -const page = { - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, -}; - -describe('zod-helper', () => { - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - let db: IMemoryDb; - let zodInputQuerySchemaTest: ZodInputQuerySchema; - type zodInputQuerySchema = z.infer; - type TypeFilterInputQuery = zodInputQuerySchema['filter']; - type TypeIncludeInputQuery = zodInputQuerySchema['include']; - type TypeFieldsInputQuery = zodInputQuerySchema['fields']; - type TypePageInputQuery = zodInputQuerySchema['page']; - - let zodQuerySchemaTest: ZodQuerySchema; - type zodQuerySchema = z.infer; - type TypeFilterQuery = zodQuerySchema['filter']; - type TypeIncludeQuery = zodQuerySchema['include']; - type TypeFieldsQuery = zodQuerySchema['fields']; - type TypePageQuery = zodQuerySchema['page']; - type TypeSortQuery = zodQuerySchema['sort']; - const dataCheckDefault = { - [QueryField.filter]: null, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: page[QueryField.page], - }; - - let zodInputPostSchemaTest: ZodInputPostSchema; - type zodInputPostSchema = z.infer; - - let zodInputPatchSchemaTest: ZodInputPatchSchema; - type zodInputPatchSchema = z.infer; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [...providerEntities(getDataSourceToken())], - }).compile(); - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - - const user = await pullUser(userRepository); - const userWithRelation = await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - zodInputQuerySchemaTest = zodInputQuerySchema(userRepository); - zodQuerySchemaTest = zodQuerySchema(userRepository); - zodInputPostSchemaTest = zodInputPostSchema(userRepository); - zodInputPatchSchemaTest = zodInputPatchSchema(userRepository); - }); - - describe('Test input query schema', () => { - it('Empty object is correct', () => { - const check = {}; - const result = zodInputQuerySchemaTest.parse(check); - expect(result).toEqual({ - ...check, - ...page, - }); - }); - - describe('Test filter', () => { - it('Valid schema', () => { - const check1: TypeFilterInputQuery = { - id: '1', - firstName: { - ne: 'dfs', - eq: 'sdf', - }, - isActive: { - in: 'sdf', - }, - }; - const check2: TypeFilterInputQuery = { - addresses: { - eq: 'null', - }, - manager: { - ne: 'null', - }, - }; - - const check3: TypeFilterInputQuery = { - 'addresses.createdAt': 'sdfsdf', - 'comments.createdAt': { - eq: 'sdfsd', - ne: 'sdfsdf', - like: 'sdfsdf', - }, - }; - const arrayCheck = [check1, check2, check3]; - - for (const item of arrayCheck) { - const dataCheck: zodInputQuerySchema = { - filter: item, - ...page, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('In Valid schema', () => { - const check1 = {}; - const check2 = { - addresses: { - in: null, - }, - manager: { - like: null, - sdfsdf: 'sdfsdf', - }, - }; - const check3 = { - sdfsdf: 'sdfsdf', - }; - const check4 = { - id: '', - firstName: { - ne: '', - eq: '', - }, - isActive: { - in: '', - }, - }; - const arrayCheck = [check1, check2, check3, check4]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - filter: item, - }; - try { - zodInputQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Test include', () => { - it('Valid schema', () => { - const check1: TypeIncludeInputQuery = 'manager'; - const check2: TypeIncludeInputQuery = 'addresses,roles'; - - const arrayCheck = [check1, check2]; - - for (const item of arrayCheck) { - const dataCheck: zodInputQuerySchema = { - [QueryField.include]: item, - ...page, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('In Valid schema', () => { - const check1 = [] as unknown; - const check2 = ['dfsdfsdf', 'addresses']; - const check3 = ['addresses', 'addresses']; - const check4 = {}; - const arrayCheck = [check1, check2, check3, check4]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - include: item, - }; - try { - zodInputQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Test select field', () => { - it('Valid schema', () => { - const check1: TypeFieldsInputQuery = { - target: 'sdfsdf', - manager: 'dfsdfsdf', - }; - const check2: TypeFieldsInputQuery = { - target: 'sdfsdf', - }; - const check3: TypeFieldsInputQuery = { - addresses: 'sdfsdf', - manager: 'sdfsdf', - }; - - const arrayCheck = [check1, check2, check3]; - - for (const item of arrayCheck) { - const dataCheck: zodInputQuerySchema = { - fields: item, - ...page, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('In Valid schema', () => { - const check1 = [] as unknown[]; - const check2 = { - addresses: 'sdfsdf', - manager: 'sdfsdf', - otherField: 'dsfsdf', - }; - const check3 = { otherField: 'dsfsdf' }; - const check4 = 'sdfsdf'; - const check5 = {}; - const check6 = 'dssd'; - const arrayCheck = [check1, check2, check3, check4, check5, check6]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - fields: item, - }; - try { - zodInputQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Test page field', () => { - it('Valid schema', () => { - const check1 = page['page']; - const check2 = {}; - const check3 = undefined; - - const arrayCheck = [check1, check2, check3]; - - for (const item of arrayCheck) { - const dataCheck = { - [QueryField.page]: item, - }; - const result = zodInputQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(page); - } - }); - }); - }); - - describe('test query schema', () => { - describe('Test filter', () => { - it('Valid schema', () => { - const check1: TypeFilterQuery = { - target: null, - relation: null, - }; - const check2: TypeFilterQuery = { - target: { - id: { - in: ['123'], - }, - }, - relation: { - addresses: { - arrayField: { - some: ['dsfsdf'], - }, - }, - roles: { - id: { - eq: '123', - }, - }, - }, - }; - const arrayCheck = [check1, check2]; - for (const item of arrayCheck) { - const dataCheck = { - ...dataCheckDefault, - [QueryField.filter]: item, - }; - const result = zodQuerySchemaTest.parse(dataCheck); - expect(result).toEqual(dataCheck); - } - }); - it('Invalid schema', () => { - const check1 = {}; - const check2 = ''; - const check3: unknown[] = []; - const check4 = null; - const check5 = undefined; - const check6 = 1; - const check7 = { - dsfsdf: 'sdf', - }; - const check8 = { - target: null, - }; - const check9 = { - target: {}, - relation: null, - }; - const check10 = { - target: '', - relation: null, - }; - const check11 = { - target: [], - relation: null, - }; - const check12 = { - target: undefined, - relation: null, - }; - const check13 = { - target: 1, - relation: null, - }; - const check14 = { - target: null, - relation: {}, - }; - const check15 = { - target: null, - relation: '', - }; - const check16 = { - target: null, - relation: [], - }; - const check17 = { - target: null, - relation: undefined, - }; - const check18 = { - target: null, - relation: 1, - }; - - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - check17, - check18, - ]; - - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - const dataCheck = { - ...dataCheckDefault, - [QueryField.filter]: item, - }; - try { - zodQuerySchemaTest.parse(dataCheck); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - }); - describe('test zodInputPostSchema', () => { - it('should be ok', () => { - const real = 123.123; - const date = new Date(); - const attributes = { - lastName: 'sdfsdf', - isActive: true, - testDate: date.toISOString(), - testReal: [`${real}`], - testArrayNull: null, - }; - const relationships = { - notes: [ - { - type: 'notes', - id: 'dsfsdf', - }, - ], - }; - const check = { - data: { - type: 'users', - attributes, - relationships, - }, - }; - const check2 = { - data: { - type: 'users', - attributes, - }, - }; - const check3 = { - data: { - id: '1', - type: 'users', - attributes, - }, - }; - - const checkResult = { - data: { - type: 'users', - attributes: { - ...attributes, - ['testDate']: date, - testReal: [real], - }, - relationships, - }, - }; - const checkResult2 = { - data: { - type: 'users', - attributes: { - ...attributes, - ['testDate']: date, - testReal: [real], - }, - }, - }; - const checkResult3 = { - data: { - id: '1', - type: 'users', - attributes: { - ...attributes, - ['testDate']: date, - testReal: [real], - }, - }, - }; - - expect(zodInputPostSchemaTest.parse(check)).toEqual(checkResult); - expect(zodInputPostSchemaTest.parse(check2)).toEqual(checkResult2); - expect(zodInputPostSchemaTest.parse(check3)).toEqual(checkResult3); - }); - - it('should be not ok', () => { - const check1 = {}; - const check2 = null; - const check3: unknown[] = []; - const check4 = ''; - const check5 = { - sdf: 'sdf', - }; - const check6 = { - data: {}, - }; - const check7 = { - data: { - type: 'users', - }, - }; - const check8 = { - data: { - type: 'users', - attributes: { - lastName: 'sdfsdf', - isActive: true, - }, - relationships: { - notes: [ - { - type: 'sdfsdf', - id: 'dsfsdf', - }, - ], - }, - }, - }; - const check9 = { - data: { - type: 'users', - attributes: { - lastName: 'sdfsdf', - id: 1, - }, - }, - }; - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodInputPostSchemaTest.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('test zodInputPatchSchema', () => { - it('should be ok', () => { - const attributes = { - lastName: 'sdfsdf', - isActive: true, - }; - const relationships = { - notes: [], - manager: null, - }; - const check = { - data: { - id: '1', - type: 'users', - attributes, - relationships, - }, - }; - const check2 = { - data: { - id: '1', - type: 'users', - attributes, - }, - }; - const check3 = { - data: { - id: '1', - type: 'users', - relationships, - }, - }; - - expect(zodInputPatchSchemaTest.parse(check)).toEqual(check); - expect(zodInputPatchSchemaTest.parse(check2)).toEqual(check2); - expect(zodInputPatchSchemaTest.parse(check3)).toEqual({ - data: { - id: '1', - type: 'users', - relationships, - attributes: {}, - }, - }); - }); - - it('should be not ok', () => { - const check1 = { - data: { - type: 'users', - attributes: { - lastName: 'sdfsdf', - isActive: true, - }, - }, - }; - const check2 = { - data: { - id: 'sdfsdf', - type: 'users', - attributes: { - lastName: 'sdfsdf', - isActive: true, - }, - }, - }; - const check3 = { - data: { - id: '1', - type: 'users', - attributes: { - id: 1, - isActive: true, - }, - }, - }; - const check4 = { - data: { - id: '1', - type: 'users', - }, - }; - const arrayCheck = [check1, check2, check3, check4]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodInputPatchSchemaTest.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('test zodInputPostRelationshipSchema', () => { - const check = { - data: [{ type: 'type', id: 'id' }], - }; - const check1 = { - data: { type: 'type', id: 'id' }, - }; - it('should be ok', () => { - expect(zodInputPostRelationshipSchema.parse(check)).toEqual(check); - expect(zodInputPostRelationshipSchema.parse(check1)).toEqual(check1); - }); - it('should be not ok', () => { - const check = {}; - const check1 = { - data: { type: 'type', id: 'id' }, - tes: 'sdfsdf', - }; - expect.assertions(2); - try { - zodInputPostRelationshipSchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - try { - zodInputPostRelationshipSchema.parse(check1); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); - }); - - describe('test zodInputPatchRelationshipSchema', () => { - const check = { - data: [], - }; - const check1 = { - data: null, - }; - it('should be ok', () => { - expect(zodInputPatchRelationshipSchema.parse(check)).toEqual(check); - expect(zodInputPatchRelationshipSchema.parse(check1)).toEqual(check1); - }); - it('should be not ok', () => { - const check = {}; - const check1 = { - data: { type: 'type', id: 'id' }, - tes: 'sdfsdf', - }; - expect.assertions(2); - try { - zodInputPatchRelationshipSchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - try { - zodInputPatchRelationshipSchema.parse(check1); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts deleted file mode 100644 index a2177f1b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-helper.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { Repository } from 'typeorm'; -import { z, ZodObject, ZodUnion } from 'zod'; -import { QueryField } from 'json-shared-type'; - -import { - getField, - getPropsTreeForRepository, - fromRelationTreeToArrayName, - ResultGetField, - getArrayPropsForEntity, - getRelationTypeArray, - getRelationTypeName, - getRelationTypePrimaryColumn, - getFieldWithType, - getTypePrimaryColumn, - getTypeForAllProps, - FieldWithType, - getPropsFromDb, -} from '../orm'; -import { Entity } from '../../types'; - -import { - ZodInputQueryShape, - zodSortInputQuerySchema, - zodPageInputQuerySchema, - zodIncludeInputQuerySchema, - zodSelectFieldsInputQuerySchema, - zodFilterInputQuerySchema, -} from './zod-input-query-schema'; - -import { - ZodQueryShape, - zodFilterQuerySchema, - zodSelectFieldsQuerySchema, - zodIncludeQuerySchema, - zodSortQuerySchema, - zodPageQuerySchema, -} from './zod-query-schema'; - -import { - PostShape, - zodAttributesSchema, - zodRelationshipsSchema, - zodTypeSchema, -} from './zod-input-post-schema'; - -import { - PatchShape, - PatchShapeDefault, - zodPatchRelationshipsSchema, -} from './zod-input-patch-schema'; - -import { - postRelationshipSchema, - PostRelationshipSchema, -} from './zod-input-post-relationship-schema'; - -import { - patchRelationshipSchema, - PatchRelationshipSchema, -} from './zod-input-patch-relationship-schema'; - -import { camelToKebab, getEntityName, ObjectTyped } from '../utils'; -import { zodIdSchema } from './zod-input-post-schema/id'; - -export { QueryField }; - -export type ZodInputQuerySchema = ZodObject< - ZodInputQueryShape, - 'strict' ->; - -export type InputQuery = z.infer>; - -export type ZodQuerySchema = ZodObject< - ZodQueryShape, - 'strict' ->; - -export type Query = z.infer>; - -export type TypeInputProps< - E extends Entity, - K extends keyof ZodInputQueryShape -> = z.infer[K]>; - -export type ZodInputPostSchema = ZodObject< - { - data: ZodObject, 'strict'>; - }, - 'strict' ->; - -export type PostData = z.infer>['data']; - -export type ZodInputPatchSchema = ZodObject< - { - data: ZodUnion< - [ - ZodObject, 'strict'>, - ZodObject, 'strict'> - ] - >; - }, - 'strict' ->; - -export type PatchData = z.infer< - ZodInputPatchSchema ->['data']; - -export type ZodInputPostRelationshipSchema = ZodObject< - { - data: PostRelationshipSchema; - }, - 'strict' ->; - -export type PostRelationshipData = - z.infer['data']; - -export type ZodInputPatchRelationshipSchema = ZodObject< - { - data: PatchRelationshipSchema; - }, - 'strict' ->; - -export type PatchRelationshipData = - z.infer['data']; - -export const zodInputQuerySchema = ( - repository: Repository -): ZodInputQuerySchema => { - const { field, relations } = getField(repository); - const relationTree = fromRelationTreeToArrayName( - getPropsTreeForRepository(repository) - ); - - const zodInputQueryShape: ZodInputQueryShape = { - [QueryField.filter]: zodFilterInputQuerySchema( - field, - relations, - relationTree - ).optional(), - [QueryField.fields]: - zodSelectFieldsInputQuerySchema(relations).optional(), - [QueryField.include]: zodIncludeInputQuerySchema.optional(), - [QueryField.sort]: zodSortInputQuerySchema.optional(), - [QueryField.page]: zodPageInputQuerySchema, - }; - return z - .object(zodInputQueryShape) - .strict( - `Query object should contain only allow params: "${Object.keys( - QueryField - ).join('"."')}"` - ); -}; - -export const zodQuerySchema = ( - repository: Repository -): ZodQuerySchema => { - const { field, relations } = getField(repository); - const relationTree = getPropsTreeForRepository(repository); - const propsArray = getArrayPropsForEntity(repository); - const typeProps = getTypeForAllProps(repository); - - const zodQueryShape: ZodQueryShape = { - [QueryField.filter]: zodFilterQuerySchema( - field, - relationTree, - propsArray, - typeProps - ), - [QueryField.fields]: zodSelectFieldsQuerySchema( - field, - relationTree - ).nullable(), - [QueryField.include]: - zodIncludeQuerySchema['relations']>( - relations - ).nullable(), - [QueryField.sort]: zodSortQuerySchema(field, relationTree).nullable(), - [QueryField.page]: zodPageQuerySchema, - }; - - return z.object(zodQueryShape).strict(); -}; - -export const zodInputPostSchema = ( - repository: Repository -): ZodInputPostSchema => { - const relationArrayProps = getRelationTypeArray(repository); - const relationPopsName = getRelationTypeName(repository); - const primaryColumnType = getRelationTypePrimaryColumn(repository); - const primaryType = getTypePrimaryColumn(repository); - const fieldWithType = ObjectTyped.entries(getFieldWithType(repository)) - .filter( - ([key]) => key !== repository.metadata.primaryColumns[0].propertyName - ) - .reduce( - (acum, [key, type]) => ({ - ...acum, - [key]: type, - }), - {} as FieldWithType - ); - const typeName = camelToKebab(getEntityName(repository.target)); - - const propsDb = getPropsFromDb(repository); - - const postShape: PostShape = { - id: zodIdSchema(primaryType).optional(), - type: zodTypeSchema(typeName), - attributes: zodAttributesSchema(fieldWithType, propsDb), - relationships: zodRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ).optional(), - }; - - return z - .object({ - data: z.object(postShape).strict(), - }) - .strict(); -}; - -export const zodInputPatchSchema = ( - repository: Repository -): ZodInputPatchSchema => { - const relationArrayProps = getRelationTypeArray(repository); - const relationPopsName = getRelationTypeName(repository); - const primaryColumnType = getRelationTypePrimaryColumn(repository); - const primaryType = getTypePrimaryColumn(repository); - - const fieldWithType = ObjectTyped.entries(getFieldWithType(repository)) - .filter( - ([key]) => key !== repository.metadata.primaryColumns[0].propertyName - ) - .reduce( - (acum, [key, type]) => ({ - ...acum, - [key]: type, - }), - {} as FieldWithType - ); - const typeName = camelToKebab(getEntityName(repository.target)); - const propsDb = getPropsFromDb(repository); - - const patchShapeDefault: PatchShapeDefault = { - id: zodIdSchema(primaryType), - type: zodTypeSchema(typeName), - attributes: zodAttributesSchema(fieldWithType, propsDb), - relationships: zodPatchRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ).optional(), - }; - - const patchShape: PatchShape = { - id: zodIdSchema(primaryType), - type: zodTypeSchema(typeName), - attributes: zodAttributesSchema(fieldWithType, propsDb) - .optional() - .default({} as any), - relationships: zodPatchRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ), - }; - - return z - .object({ - data: z.union([ - z.object(patchShapeDefault).strict(), - z.object(patchShape).strict(), - ]), - }) - .strict(); -}; - -export const zodInputPostRelationshipSchema: ZodInputPostRelationshipSchema = z - .object({ - data: postRelationshipSchema, - }) - .strict(); - -export const zodInputPatchRelationshipSchema: ZodInputPatchRelationshipSchema = - z - .object({ - data: patchRelationshipSchema, - }) - .strict(); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts deleted file mode 100644 index a3cd25bd..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { patchRelationshipSchema, PatchRelationshipSchema } from './'; -import { ZodError } from 'zod'; - -describe('zod-input-patch-relationship-schema', () => { - it('should be ok', () => { - const check = { - type: 'type', - id: 'id', - }; - const check1 = [ - { - type: 'type', - id: 'id', - }, - ]; - const check2 = null; - const check3: any[] = []; - expect(patchRelationshipSchema.parse(check)).toEqual(check); - expect(patchRelationshipSchema.parse(check1)).toEqual(check1); - expect(patchRelationshipSchema.parse(check2)).toEqual(check2); - expect(patchRelationshipSchema.parse(check3)).toEqual(check3); - }); - it('should be not ok', () => { - const check = { - asd: 'sdfsdf', - }; - - const check2 = { - sdfs: 'dsfsdf', - type: 'type', - id: 'id', - }; - const check4 = true; - const check5 = 'dsfsdf'; - - const checkArray = [check, check2, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - patchRelationshipSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts deleted file mode 100644 index ae69387e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-relationship-schema/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z, ZodArray, ZodNullable, ZodUnion } from 'zod'; - -import { Data, data } from '../zod-input-post-relationship-schema'; - -export type PatchRelationshipSchema = ZodUnion< - [ZodNullable, ZodArray] ->; -export const patchRelationshipSchema: PatchRelationshipSchema = z.union([ - data.nullable(), - z.array(data), -]); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts deleted file mode 100644 index 6ce1600b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Entity } from '../../../types'; -import { ZodAttributesSchema } from '../zod-input-post-schema/attributes'; -import { ZodTypeSchema } from '../zod-input-post-schema/type'; -import { ZodIdSchema } from '../zod-input-post-schema/id'; - -import { ZodDefault, ZodObject, ZodOptional } from 'zod'; -import { - ZodPatchRelationshipsSchema, - zodPatchRelationshipsSchema, -} from './relationships'; - -export type PatchShape = { - id: ZodIdSchema; - type: ZodTypeSchema; - attributes: ZodDefault>>; - relationships: ZodPatchRelationshipsSchema; -}; - -export type PatchShapeDefault = { - id: ZodIdSchema; - type: ZodTypeSchema; - attributes: ZodAttributesSchema; - relationships: ZodOptional>; -}; - -export type ZodPatchData = ZodObject, 'strict'>; -export { zodPatchRelationshipsSchema }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts deleted file mode 100644 index d04c1f23..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { - zodPatchRelationshipsSchema, - ZodPatchRelationshipsSchema, -} from './relationships'; -import { Users } from '../../../mock-utils'; -import { - RelationPropsType, - RelationPropsTypeName, - RelationPrimaryColumnType, - TypeField, -} from '../../orm'; - -describe('zodRelationshipsSchema', () => { - const relationArrayProps: RelationPropsType = { - roles: true, - userGroup: false, - notes: true, - addresses: false, - comments: true, - manager: false, - }; - const relationPopsName: RelationPropsTypeName = { - roles: 'Roles', - userGroup: 'UserGroups', - notes: 'Notes', - addresses: 'Addresses', - comments: 'Comments', - manager: 'Users', - }; - - const primaryColumnType: RelationPrimaryColumnType = { - roles: TypeField.number, - userGroup: TypeField.number, - notes: TypeField.string, - addresses: TypeField.number, - comments: TypeField.number, - manager: TypeField.number, - }; - - let relationshipsSchema: ZodPatchRelationshipsSchema; - beforeAll(() => { - relationshipsSchema = zodPatchRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ); - }); - - it('Should be ok', () => { - const check = { - comments: [ - { - type: 'comments', - id: '1', - }, - ], - userGroup: { - type: 'user-groups', - id: '1', - }, - manager: { - type: 'users', - id: '1', - }, - notes: [ - { - type: 'notes', - id: 'id', - }, - ], - }; - const check2 = { - comments: [], - manager: null, - }; - const check3 = { - comments: { - data: [ - { - type: 'comments', - id: '1', - }, - ], - }, - userGroup: { - data: { - type: 'user-groups', - id: '1', - }, - }, - manager: { - data: { - type: 'users', - id: '1', - }, - }, - notes: { - data: [ - { - type: 'notes', - id: 'id', - }, - ], - }, - }; - - expect(relationshipsSchema.parse(check)).toEqual(check); - expect(relationshipsSchema.parse(check2)).toEqual(check2); - expect(relationshipsSchema.parse(check3)).toEqual(check); - }); - - it('should be not ok', () => { - const check1 = {}; - const check2 = ''; - const check3: any[] = []; - const check4 = true; - const check5 = { - sddsf: {}, - }; - const check6 = { - comments: null, - }; - const check7 = { - comments: {}, - }; - const check8 = { - comments: '', - }; - const check9 = { - comments: true, - }; - const check10 = { - comments: [ - { - sdsf: 'sdfsdf', - }, - ], - }; - const check11 = { - comments: [{}], - }; - const check12 = { - manager: {}, - }; - const check13 = { - manager: { - sdfs: 'sdsdf', - }, - }; - const check14 = { - manager: { - id: 'sdsdf', - type: 'users', - }, - }; - const check15 = { - manager: [], - }; - const check16 = { - comments: null, - }; - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - relationshipsSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts deleted file mode 100644 index 4e9e48dd..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-patch-schema/relationships.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { - ZodArray, - ZodEffects, - ZodObject, - ZodOptional, - z, - ZodNullable, -} from 'zod'; -import { Entity, EntityRelation } from '../../../types'; -import { - RelationPrimaryColumnType, - RelationPropsType, - RelationPropsTypeName, -} from '../../orm'; - -import { zodDataSchema, ZodDataSchema } from '../zod-input-post-schema/data'; -import { nonEmptyObject } from '../zod-utils'; -import { ObjectTyped, camelToKebab } from '../../utils'; - -export type DataArray = ZodArray>; - -export type DataItem = ZodOptional< - E extends true ? DataArray : ZodNullable> ->; - -export type ShapeRelationships = { - [K in keyof RelationPropsType]: DataItem[K]>; -}; - -export type ZodPatchRelationshipsSchema = ZodEffects< - ZodObject, 'strict'> ->; -export const zodPatchRelationshipsSchema = ( - relationArrayProps: RelationPropsType, - relationPopsName: RelationPropsTypeName, - primaryColumnType: RelationPrimaryColumnType -): ZodPatchRelationshipsSchema => { - const shape = ObjectTyped.entries(relationArrayProps).reduce( - (acum, [props, value]: [EntityRelation, boolean]) => { - const typeName = camelToKebab(relationPopsName[props]); - const primaryType = primaryColumnType[props]; - const zodDataSchemaObject = zodDataSchema(typeName, primaryType); - const dataItem: DataItem = ( - value ? z.array(zodDataSchemaObject) : zodDataSchemaObject.nullable() - ).optional(); - return { - ...acum, - [props]: z.union([ - dataItem, - z - .object({ data: dataItem }) - .strict() - .refine(nonEmptyObject()) - .transform((i) => { - const { data } = i; - return data; - }), - ]), - }; - }, - {} as ShapeRelationships - ); - return z.object(shape).strict().refine(nonEmptyObject()); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts deleted file mode 100644 index 3fe1aec4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { postRelationshipSchema, PostRelationshipSchema } from './'; -import { ZodError } from 'zod'; - -describe('zod-input-post-relationship-schema', () => { - it('should be ok', () => { - const check = { - type: 'type', - id: 'id', - }; - const check1 = [ - { - type: 'type', - id: 'id', - }, - ]; - expect(postRelationshipSchema.parse(check)).toEqual(check); - expect(postRelationshipSchema.parse(check1)).toEqual(check1); - }); - it('should be not ok', () => { - const check = { - asd: 'sdfsdf', - }; - const check1: any[] = []; - const check2 = { - sdfs: 'dsfsdf', - type: 'type', - id: 'id', - }; - const check3 = null; - const check4 = true; - const check5 = 'dsfsdf'; - - const checkArray = [check, check1, check2, check3, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - postRelationshipSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts deleted file mode 100644 index 0386b8b1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-relationship-schema/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { z, ZodArray, ZodObject, ZodString, ZodUnion } from 'zod'; - -export type Data = ZodObject< - { - type: ZodString; - id: ZodString; - }, - 'strict' ->; -export const data: Data = z - .object({ - type: z.string(), - id: z.string(), - }) - .strict(); - -export type PostRelationshipSchema = ZodUnion< - [Data, ZodArray] ->; -export const postRelationshipSchema: PostRelationshipSchema = z.union([ - data, - z.array(data).nonempty(), -]); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts deleted file mode 100644 index 9748a761..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { z, ZodError } from 'zod'; -import { zodAttributesSchema, ZodAttributesSchema } from './attributes'; -import { Addresses, Users } from '../../../mock-utils'; -import { FieldWithType, PropsForField, TypeField } from '../../orm'; - -describe('attributes', () => { - let schemaUsers: ZodAttributesSchema; - let schemaAddresses: ZodAttributesSchema; - type SchemaTypeUsers = z.infer>; - type SchemaTypeAddresses = z.infer>; - beforeAll(() => { - const fieldTypeUsers: FieldWithType = { - id: TypeField.number, - isActive: TypeField.boolean, - firstName: TypeField.string, - createdAt: TypeField.date, - lastName: TypeField.string, - login: TypeField.string, - testDate: TypeField.date, - updatedAt: TypeField.date, - testReal: TypeField.array, - testArrayNull: TypeField.array, - }; - const propsDb: PropsForField = { - id: { type: Number, isArray: false, isNullable: false }, - login: { type: 'varchar', isArray: false, isNullable: false }, - firstName: { type: 'varchar', isArray: false, isNullable: true }, - testReal: { type: 'real', isArray: true, isNullable: false }, - testArrayNull: { type: 'real', isArray: true, isNullable: true }, - lastName: { type: 'varchar', isArray: false, isNullable: true }, - isActive: { type: 'boolean', isArray: false, isNullable: true }, - createdAt: { type: 'timestamp', isArray: false, isNullable: true }, - testDate: { type: 'timestamp', isArray: false, isNullable: true }, - updatedAt: { type: 'timestamp', isArray: false, isNullable: true }, - }; - const fieldTypeAddresses: FieldWithType = { - id: TypeField.number, - arrayField: TypeField.array, - state: TypeField.string, - city: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - country: TypeField.string, - }; - schemaUsers = zodAttributesSchema(fieldTypeUsers, propsDb); - schemaAddresses = zodAttributesSchema( - fieldTypeAddresses, - {} as PropsForField - ); - }); - - it('should be ok', () => { - const check: SchemaTypeUsers = { - isActive: true, - lastName: 'sdsdf', - testReal: [123.123, 123.123], - }; - const check2: SchemaTypeAddresses = { - arrayField: ['test', 'test'], - }; - const date = new Date(); - const check3 = { - testDate: date.toISOString(), - }; - expect(schemaUsers.parse(check)).toEqual(check); - expect(schemaAddresses.parse(check2)).toEqual(check2); - expect(schemaUsers.parse(check3)).toEqual({ - testDate: date, - }); - }); - - it('should be not ok', () => { - const check = { - id: '1', - isActive: 'true', - lastName: 1, - }; - const check2 = { - arrayField: 'test', - }; - expect.assertions(2); - try { - expect(schemaUsers.parse(check)).toEqual(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - try { - schemaAddresses.parse(check2); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts deleted file mode 100644 index 58e0d2c4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/attributes.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { - ZodArray, - ZodBoolean, - ZodEnum, - ZodNumber, - ZodString, - z, - ZodDate, - ZodObject, - ZodEffects, - ZodOptional, - ZodNullable, - ZodType, -} from 'zod'; - -import { Entity } from '../../../types'; -import { - FieldWithType, - PropsFieldItem, - PropsForField, - TypeField, -} from '../../orm'; -import { ObjectTyped } from '../../utils'; -import { nonEmptyObject } from '../zod-utils'; - -const literalSchema = z.union([z.string(), z.number(), z.boolean()]); - -const getZodSchemaForJson = (isNull: boolean) => { - const tmpSchema = isNull ? literalSchema.nullable() : literalSchema; - const jsonSchema: any = z.lazy(() => - z.union([ - tmpSchema, - z.array(jsonSchema.nullable()), - z.record(jsonSchema.nullable()), - ]) - ); - - return jsonSchema; -}; - -type Literal = ReturnType; - -type Json = Literal | { [key: string]: Json } | Json[]; - -type ZodTypeForArray = - | ZodString - | ZodDate - | ZodEffects - | ZodBoolean; -type ZodArrayType = - | ZodArray - | ZodNullable>; - -type TypeMapToZod = { - [TypeField.array]: ZodOptional; - [TypeField.date]: ZodOptional>; - [TypeField.number]: ZodOptional< - | ZodEffects - | ZodNullable> - >; - [TypeField.boolean]: ZodOptional>; - [TypeField.string]: ZodOptional< - | ZodString - | ZodEnum<[string, ...string[]]> - | ZodNullable> - >; - [TypeField.object]: ZodType | ZodNullable>; -}; - -type ZodShapeAttributes = Omit< - { - [K in keyof FieldWithType]: TypeMapToZod[FieldWithType[K]]; - }, - 'id' ->; - -export type ZodAttributesSchema = ZodEffects< - ZodObject, 'strict'> ->; - -function getZodSchemaForArray(props: PropsFieldItem): ZodTypeForArray { - if (!props) return z.string(); - let zodSchema: ZodTypeForArray; - switch (props.type) { - case 'number': - case 'real': - case 'integer': - case 'bigint': - case 'double': - case 'numeric': - case Number: - zodSchema = z.preprocess((x) => Number(x), z.number()); - break; - case 'date': - case Date: - zodSchema = z.coerce.date(); - break; - case 'boolean': - case Boolean: - zodSchema = z.boolean(); - break; - default: - zodSchema = z.string(); - } - - return zodSchema; -} - -export const zodAttributesSchema = ( - fieldWithType: FieldWithType, - propsDb: PropsForField -): ZodAttributesSchema => { - const shape = ObjectTyped.entries(fieldWithType).reduce( - (acum, [props, type]: [keyof FieldWithType, TypeField]) => { - let zodShema: TypeMapToZod[typeof type]; - const propsDbType = propsDb[props]; - switch (type) { - case TypeField.array: { - const tmpSchema = getZodSchemaForArray(propsDbType).array(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.date: { - const tmpSchema = z.coerce.date(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.number: { - const tmpSchema = z.preprocess((x) => Number(x), z.number()); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.boolean: { - const tmpSchema = z.boolean(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - case TypeField.object: { - zodShema = getZodSchemaForJson(propsDbType.isNullable).optional(); - break; - } - case TypeField.string: { - const tmpSchema = z.string(); - zodShema = ( - propsDbType && propsDbType.isNullable - ? tmpSchema.nullable() - : tmpSchema - ).optional(); - break; - } - } - - return { - ...acum, - [props]: zodShema, - }; - }, - {} as ZodShapeAttributes - ); - - return z.object(shape).strict().refine(nonEmptyObject); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts deleted file mode 100644 index 00f409d7..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { zodDataSchema, ZodDataSchema } from './data'; -import { TypeField } from '../../orm'; -import { ZodError } from 'zod'; - -describe('zodDataSchema', () => { - let zodData: ZodDataSchema; - beforeAll(() => { - zodData = zodDataSchema('users', TypeField.string); - }); - - it('Should be ok', () => { - const check = { - type: 'users', - id: 'id', - }; - expect(zodData.parse(check)).toEqual(check); - }); - - it('Should be not ok', () => { - const check = {}; - const check1 = { - test: '1', - }; - const check3: any[] = []; - const check4 = 'adfsdf'; - const check5 = true; - const checkArray = [check, check1, check3, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - zodData.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts deleted file mode 100644 index 71608126..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/data.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { z, ZodEffects, ZodObject } from 'zod'; - -import { ZodTypeSchema, zodTypeSchema } from './type'; -import { ZodIdSchema, zodIdSchema } from './id'; -import { TypeForId } from '../../orm'; -import { nonEmptyObject } from '../zod-utils'; - -export type ZodDataSchema = ZodEffects< - ZodObject<{ - id: ZodIdSchema; - type: ZodTypeSchema; - }> ->; -export const zodDataSchema = ( - type: string, - typeId: TypeForId -): ZodDataSchema => - z - .object({ - id: zodIdSchema(typeId), - type: zodTypeSchema(type), - }) - .strict() - .refine(nonEmptyObject); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts deleted file mode 100644 index 801a7c38..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { zodIdSchema, ZodIdSchema } from './id'; -import { TypeField } from '../../orm'; -import { ZodError } from 'zod'; - -describe('zodIdSchema', () => { - let numberStringSchema: ZodIdSchema; - let stringSchema: ZodIdSchema; - beforeAll(() => { - numberStringSchema = zodIdSchema(TypeField.number); - stringSchema = zodIdSchema(TypeField.string); - }); - - it('Should be correct', () => { - const check1 = '1'; - const check2 = '12'; - const check3 = '123'; - const check4 = '-123'; - - const check5 = 'sfdsf'; - const checkArray = [check1, check2, check3, check4]; - for (const item of checkArray) { - expect(numberStringSchema.parse(item)).toBe(item); - } - expect(stringSchema.parse(check5)).toBe(check5); - }); - - it('Should be not ok', () => { - expect.assertions(1); - - try { - numberStringSchema.parse('sdfdfsfsf'); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts deleted file mode 100644 index ef734c03..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/id.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { z, ZodString } from 'zod'; -import { TypeField, TypeForId } from '../../orm'; - -const reg = new RegExp('^-?\\d+$'); - -export type ZodIdSchema = ZodString; -export const zodIdSchema = (typeId: TypeForId): ZodIdSchema => { - let idSchema = z.string(); - if (typeId === TypeField.number) { - idSchema = idSchema.regex(reg); - } - - return idSchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts deleted file mode 100644 index fb6289e0..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ZodObject, ZodOptional } from 'zod'; - -import { Entity } from '../../../types'; - -import { zodAttributesSchema, ZodAttributesSchema } from './attributes'; -import { zodTypeSchema, ZodTypeSchema } from './type'; -import { - zodRelationshipsSchema, - ZodRelationshipsSchema, -} from './relationships'; -import { ZodIdSchema } from './id'; - -export type PostShape = { - id: ZodOptional; - attributes: ZodAttributesSchema; - type: ZodTypeSchema; - relationships: ZodOptional>; -}; - -export type ZodPostData = ZodObject, 'strict'>; - -export { zodAttributesSchema, zodTypeSchema, zodRelationshipsSchema }; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts deleted file mode 100644 index 54604aba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { - zodRelationshipsSchema, - ZodRelationshipsSchema, -} from './relationships'; -import { Users } from '../../../mock-utils'; -import { - RelationPropsType, - RelationPropsTypeName, - RelationPrimaryColumnType, - TypeField, -} from '../../orm'; - -describe('zodRelationshipsSchema', () => { - const relationArrayProps: RelationPropsType = { - roles: true, - userGroup: false, - notes: true, - addresses: false, - comments: true, - manager: false, - }; - const relationPopsName: RelationPropsTypeName = { - roles: 'Roles', - userGroup: 'UserGroups', - notes: 'Notes', - addresses: 'Addresses', - comments: 'Comments', - manager: 'Users', - }; - - const primaryColumnType: RelationPrimaryColumnType = { - roles: TypeField.number, - userGroup: TypeField.number, - notes: TypeField.string, - addresses: TypeField.number, - comments: TypeField.number, - manager: TypeField.number, - }; - - let relationshipsSchema: ZodRelationshipsSchema; - beforeAll(() => { - relationshipsSchema = zodRelationshipsSchema( - relationArrayProps, - relationPopsName, - primaryColumnType - ); - }); - - it('Should be ok', () => { - const check = { - comments: [ - { - type: 'comments', - id: '1', - }, - ], - userGroup: { - type: 'user-groups', - id: '1', - }, - manager: { - type: 'users', - id: '1', - }, - notes: [ - { - type: 'notes', - id: 'id', - }, - ], - }; - const check2 = { - comments: { - data: [ - { - type: 'comments', - id: '1', - }, - ], - }, - userGroup: { - data: { - type: 'user-groups', - id: '1', - }, - }, - manager: { - data: { - type: 'users', - id: '1', - }, - }, - notes: { - data: [ - { - type: 'notes', - id: 'id', - }, - ], - }, - }; - expect(relationshipsSchema.parse(check)).toEqual(check); - expect(relationshipsSchema.parse(check2)).toEqual(check); - }); - - it('should be not ok', () => { - const check1 = {}; - const check2 = ''; - const check3: any[] = []; - const check4 = true; - const check5 = { - sddsf: {}, - }; - const check6 = { - comments: [], - }; - const check7 = { - comments: {}, - }; - const check8 = { - comments: '', - }; - const check9 = { - comments: true, - }; - const check10 = { - comments: [ - { - sdsf: 'sdfsdf', - }, - ], - }; - const check11 = { - comments: [{}], - }; - const check12 = { - manager: {}, - }; - const check13 = { - manager: { - sdfs: 'sdsdf', - }, - }; - const check14 = { - manager: { - id: 'sdsdf', - type: 'users', - }, - }; - const check15 = { - manager: null, - }; - const check16 = { - manager: [], - }; - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - relationshipsSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts deleted file mode 100644 index 3cad63e6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/relationships.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ZodArray, ZodEffects, ZodObject, ZodOptional, z } from 'zod'; -import { Entity, EntityRelation } from '../../../types'; -import { - ArrayPropsForEntity, - RelationPrimaryColumnType, - RelationPropsType, - RelationPropsTypeName, -} from '../../orm'; - -import { zodDataSchema, ZodDataSchema } from './data'; -import { nonEmptyObject } from '../zod-utils'; -import { ObjectTyped, camelToKebab } from '../../utils'; - -export type PropsArray = Omit< - ArrayPropsForEntity, - 'target' ->; - -export type DataArray = ZodArray< - ZodDataSchema, - 'atleastone' ->; - -export type DataItem = ZodOptional< - E extends true ? DataArray : ZodDataSchema ->; - -export type ShapeRelationships = { - [K in keyof RelationPropsType]: DataItem[K]>; -}; - -export type ZodRelationshipsSchema = ZodEffects< - ZodObject, 'strict'> ->; -export const zodRelationshipsSchema = ( - relationArrayProps: RelationPropsType, - relationPopsName: RelationPropsTypeName, - primaryColumnType: RelationPrimaryColumnType -): ZodRelationshipsSchema => { - const shape = ObjectTyped.entries(relationArrayProps).reduce( - (acum, [props, value]: [EntityRelation, boolean]) => { - const typeName = camelToKebab(relationPopsName[props]); - const primaryType = primaryColumnType[props]; - const zodDataSchemaObject = zodDataSchema(typeName, primaryType); - const dataItem: DataItem = ( - value ? z.array(zodDataSchemaObject).nonempty() : zodDataSchemaObject - ).optional(); - return { - ...acum, - [props]: z.union([ - dataItem, - z - .object({ data: dataItem }) - .strict() - .refine(nonEmptyObject()) - .transform((i) => { - const { data } = i; - return data; - }), - ]), - }; - }, - {} as ShapeRelationships - ); - - return z.object(shape).strict().refine(nonEmptyObject()); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts deleted file mode 100644 index 9023573c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { zodTypeSchema, ZodTypeSchema } from './type'; -import { ZodError } from 'zod'; - -describe('type', () => { - const literal = 'users'; - let userTypeSchema: ZodTypeSchema; - beforeAll(() => { - userTypeSchema = zodTypeSchema(literal); - }); - it('should be ok', () => { - expect(userTypeSchema.parse(literal)).toEqual(literal); - }); - it('should be ok', () => { - expect.assertions(1); - try { - userTypeSchema.parse('test'); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts deleted file mode 100644 index 84290c0f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-post-schema/type.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { z, ZodLiteral } from 'zod'; - -export type ZodTypeSchema = ZodLiteral; -export const zodTypeSchema = (type: T) => z.literal(type); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts deleted file mode 100644 index bb737d35..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.spec.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { z, ZodError } from 'zod'; -import { - zodFilterFieldSchema, - zodFilterRelationSchema, - zodFilterSchema, -} from './filter'; - -describe('check "filter"', () => { - describe('Check "zodFilterRelationSchema"', () => { - it('Valid check', () => { - const check1: z.infer = { eq: 'null' }; - const check2: z.infer = { ne: 'null' }; - - const arrayCheck = [check1, check2]; - - for (const item of arrayCheck) { - const result = zodFilterRelationSchema.parse(item); - expect(result).toEqual(item); - } - }); - - it('Invalid check', () => { - const check1 = { eq: null, ne: null }; - const check2 = { eq: 123123 }; - const check3 = { eq: '123123' }; - const check4 = { ne: '123123' }; - const check5 = { ne: true }; - const check6 = { ne: 'true' }; - const check7 = { sdfsdf: 'true' }; - - const arrayCheck = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - ]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodFilterRelationSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Check "zodFilterFieldSchema:', () => { - it('Valid schema', () => { - const check1: z.infer = { - some: 'sdf', - ne: 'sdfsdf', - }; - const check2: z.infer = { - eq: 'sdf', - ne: 'sdfsdf', - in: 'sdsf', - }; - const check3: z.infer = { - like: 'sdsf', - }; - const check4: z.infer = 'dsfsdf'; - const arrayCheck = [check1, check2, check3, check4]; - for (const item of arrayCheck) { - const result = zodFilterFieldSchema.parse(item); - expect(result).toEqual(item); - } - }); - - it('Invalid schema', () => { - const check1 = { dfd: 'dfsdf' }; - const check2 = { dfd: 'dfsdf', like: 'sdsf' }; - const check3 = {}; - const check4 = [] as any[]; - const check5 = null; - const check6 = ''; - - const arrayCheck = [check1, check2, check3, check4, check5, check6]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodFilterFieldSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - - describe('Check "zodFilterSchema"', () => { - it('Valid schema', () => { - const check1: z.infer = { - some: 'sdf', - ne: 'sdfsdf', - }; - const check2: z.infer = { - eq: 'sdf', - ne: 'sdfsdf', - in: 'sdsf', - }; - const check3: z.infer = { - like: 'sdsf', - }; - const arrayCheck = [check1, check2, check3]; - for (const item of arrayCheck) { - const result = zodFilterSchema.parse(item); - expect(result).toEqual(item); - } - }); - - it('Invalid schema', () => { - const check1 = { dfd: 'dfsdf' }; - const check2 = { dfd: 'dfsdf', like: 'sdsf' }; - const check3 = {}; - const check4 = { in: '' }; - - const arrayCheck = [check1, check2, check3]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodFilterSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts deleted file mode 100644 index f1c87d68..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/filter.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { - z, - ZodEffects, - ZodLiteral, - ZodObject, - ZodOptional, - ZodString, - ZodUnion, -} from 'zod'; -import { ZodFilterMap } from './select'; -import { Entity, FilterOperand, ValueOf } from '../../../types'; -import { ObjectTyped } from '../../utils'; -import { - getValidationErrorForStrict, - nonEmptyObject, - oneOf, - stringLongerThan, -} from '../zod-utils'; -import { ConcatRelation, ResultGetField } from '../../orm'; - -export type ZodFilterSchema = ZodEffects< - ZodObject< - { - [key in ValueOf]: ZodOptional< - ZodEffects - >; - }, - 'strict' - > ->; -export const zodFilterSchema: ZodFilterSchema = z - .object( - ObjectTyped.values(FilterOperand).reduce((acum, item) => { - acum[item] = z.string().refine(stringLongerThan()); - return acum; - }, {} as Record>) - ) - .partial() - .strict() - .refine(oneOf(Object.values(FilterOperand)), { - message: `Must have one of: "${Object.values(FilterOperand).join('","')}"`, - }); - -export type ZodFilterFieldSchema = ZodUnion< - [ZodEffects, ZodFilterSchema] ->; -export const zodFilterFieldSchema: ZodFilterFieldSchema = z.union([ - z.string().refine(stringLongerThan()), - zodFilterSchema, -]); - -export type ZodFilterRelationSchema = ZodUnion< - [ - ZodObject<{ - [FilterOperand.eq]: ZodLiteral<'null'>; - }>, - ZodObject<{ - [FilterOperand.ne]: ZodLiteral<'null'>; - }> - ] ->; -export const zodFilterRelationSchema: ZodFilterRelationSchema = z.union([ - z - .object({ - [FilterOperand.eq]: z.literal('null'), - }) - .strict(), - z - .object({ - [FilterOperand.ne]: z.literal('null'), - }) - .strict(), -]); - -export type ZodFilterInputMap = ZodFilterMap< - ResultGetField['field'], - ZodFilterFieldSchema -> & - ZodFilterMap['relations'], ZodFilterRelationSchema> & - ZodFilterMap, ZodFilterFieldSchema>; - -export const getObjectForFilter = < - E extends readonly [string, ...string[]], - R extends boolean ->( - fieldList: E, - isRelation: R -): ZodFilterMap< - E, - R extends true ? ZodFilterRelationSchema : ZodFilterFieldSchema -> => { - return fieldList.reduce( - (acum, item) => ({ - ...acum, - ...{ - [item]: isRelation - ? zodFilterRelationSchema.optional() - : zodFilterFieldSchema.optional(), - }, - }), - {} as ZodFilterMap< - E, - R extends true ? ZodFilterRelationSchema : ZodFilterFieldSchema - > - ); -}; - -export type ZodFilterInputQueryObject = ZodObject< - ZodFilterInputMap, - 'strict' ->; - -export type ZodFilterInputQuerySchema = ZodEffects< - ZodFilterInputQueryObject ->; - -export const zodFilterInputQuerySchema = ( - field: ResultGetField['field'], - relations: ResultGetField['relations'], - relationField: ConcatRelation -): ZodFilterInputQuerySchema => { - const filterMap: ZodFilterInputMap = { - ...getObjectForFilter(field, false), - ...getObjectForFilter(relations, true), - ...getObjectForFilter(relationField, false), - }; - - const zodFilterInputQueryObject: ZodFilterInputQueryObject = z - .object(filterMap) - .strict(getValidationErrorForStrict(Object.keys(filterMap), 'Filter')); - - return zodFilterInputQueryObject.refine(nonEmptyObject(), { - message: 'Validation error: Filter should be not empty', - }); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts deleted file mode 100644 index 6b886e13..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/include.ts +++ /dev/null @@ -1,9 +0,0 @@ -// export { -// ZodIncludeQuerySchema as ZodIncludeInputQuerySchema, -// zodIncludeQuerySchema as zodIncludeInputQuerySchema, -// } from '../zod-query-schema/index'; - -import { z, ZodString } from 'zod'; - -export type ZodIncludeInputQuerySchema = ZodString; -export const zodIncludeInputQuerySchema = z.string().min(1); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts deleted file mode 100644 index 4189c7ab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ZodOptional } from 'zod'; - -import { zodFilterInputQuerySchema, ZodFilterInputQuerySchema } from './filter'; -import { - zodSelectFieldsInputQuerySchema, - ZodSelectFieldsInputQuerySchema, -} from './select'; -import { - zodIncludeInputQuerySchema, - ZodIncludeInputQuerySchema, -} from './include'; -import { zodSortInputQuerySchema, ZodSortInputQuerySchema } from './sort'; -import { zodPageInputQuerySchema, ZodPageInputQuerySchema } from './page'; - -import { Entity } from '../../../types'; -import { QueryField } from '../zod-helper'; - -export type ZodInputQueryShape = { - [QueryField.filter]: ZodOptional>; - [QueryField.fields]: ZodOptional>; - [QueryField.include]: ZodOptional; - [QueryField.sort]: ZodOptional; - [QueryField.page]: ZodPageInputQuerySchema; -}; - -export { - zodIncludeInputQuerySchema, - zodSortInputQuerySchema, - zodPageInputQuerySchema, - zodSelectFieldsInputQuerySchema, - zodFilterInputQuerySchema, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts deleted file mode 100644 index 1d3b4900..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/page.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { - zodPageQuerySchema as zodPageInputQuerySchema, - ZodPageQuerySchema as ZodPageInputQuerySchema, -} from '../zod-query-schema/index'; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts deleted file mode 100644 index 2cec1d24..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { zodSelectFieldsInputQuerySchema } from './select'; -import { z, ZodError } from 'zod'; -import { ResultGetField } from '../../orm'; -import { Users } from '../../../mock-utils'; - -describe('Check "zodSelectFieldsInputQuerySchema"', () => { - const arrayForCheck: ResultGetField['relations'] = [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', - ]; - const zodSelectFieldsSchema = - zodSelectFieldsInputQuerySchema(arrayForCheck); - type TypeSelectField = z.infer; - it('Valid schema', () => { - const check1: TypeSelectField = { - target: 'sdfsdfsfd', - notes: 'sdfsdf', - comments: 'sdfsdf', - roles: 'sdfsdf', - }; - const check2: TypeSelectField = { - target: 'sdfsdfsfd', - }; - const check3: TypeSelectField = { - addresses: 'sdfsdf', - manager: 'sdfsdf', - }; - const arrayCheck = [check1, check2, check3]; - for (const item of arrayCheck) { - const result = zodSelectFieldsSchema.parse(item); - expect(result).toEqual(item); - } - }); - it('Invalid schema', () => { - const check1 = {}; - const check2 = { dfd: 'dfsdf', like: 'sdsf' }; - const check3 = null; - const check4 = [] as any[]; - const check5 = 'ssss'; - - const arrayCheck = [check1, check2, check3, check4, check5]; - expect.assertions(arrayCheck.length); - for (const item of arrayCheck) { - try { - zodSelectFieldsSchema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts deleted file mode 100644 index f2386432..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/select.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - z, - ZodEffects, - ZodObject, - ZodOptional, - ZodString, - ZodTypeAny, -} from 'zod'; - -import { ResultGetField } from '../../orm'; -import { Entity } from '../../../types'; -import { getValidationErrorForStrict, nonEmptyObject } from '../zod-utils'; - -export type ZodSelectFieldsInputQuerySchema = ZodEffects< - ZodObject< - ObjectForSelectField['relations'], ZodString>, - 'strict' - > ->; - -export type ZodFilterMap< - R extends readonly [string, ...string[]], - F extends ZodTypeAny -> = { - [K in R[number]]: ZodOptional; -}; - -type ObjectForSelectField< - R extends readonly [string, ...string[]], - K extends ZodTypeAny -> = { - target: ZodOptional; -} & ZodFilterMap; - -const objectForSelectField = ( - relationList: R -): ObjectForSelectField => { - const relation = relationList.reduce( - (acum, item) => ({ ...acum, ...{ [item]: z.string().optional() } }), - {} as ZodFilterMap - ); - - return { - target: z.string().optional(), - ...relation, - }; -}; - -export const zodSelectFieldsInputQuerySchema = ( - relationList: ResultGetField['relations'] -): ZodSelectFieldsInputQuerySchema => { - const resultShape: ObjectForSelectField< - ResultGetField['relations'], - ZodString - > = objectForSelectField['relations']>(relationList); - - return z - .object(resultShape) - .strict(getValidationErrorForStrict(['target', ...relationList], 'Fields')) - .refine(nonEmptyObject(), { - message: 'Validation error: Need select field for target or relation', - }); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts deleted file mode 100644 index 7f44965e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-input-query-schema/sort.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { z, ZodString } from 'zod'; - -export type ZodSortInputQuerySchema = ZodString; -export const zodSortInputQuerySchema: ZodSortInputQuerySchema = z.string(); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts deleted file mode 100644 index 36f36f9a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.spec.ts +++ /dev/null @@ -1,483 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { ZodFilterQuerySchema, zodFilterQuerySchema } from './filter'; -import { - AllFieldWithTpe, - PropsArray, - RelationTree, - ResultGetField, - TypeField, -} from '../../orm'; -import { - Users, - Roles, - UserGroups, - Comments, - Notes, - Addresses, -} from '../../../mock-utils'; - -describe('Check "filter" zod schema', () => { - const relation: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], - }; - const fields: ResultGetField['field'] = [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ]; - - const usersPropsArray: PropsArray = { - testArrayNull: true, - testReal: true, - }; - const rolesPropsArray: PropsArray = {}; - const userGroupPropsArray: PropsArray = {}; - const commentsPropsArray: PropsArray = {}; - const notesPropsArray: PropsArray = {}; - const addressesPropsArray: PropsArray = { - arrayField: true, - }; - - const propsType: AllFieldWithTpe = { - id: TypeField.number, - login: TypeField.string, - firstName: TypeField.string, - lastName: TypeField.string, - isActive: TypeField.boolean, - createdAt: TypeField.date, - testReal: TypeField.array, - testArrayNull: TypeField.array, - testDate: TypeField.date, - updatedAt: TypeField.date, - addresses: { - id: TypeField.number, - city: TypeField.string, - state: TypeField.string, - country: TypeField.string, - arrayField: TypeField.array, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - manager: { - id: TypeField.number, - login: TypeField.string, - firstName: TypeField.string, - lastName: TypeField.string, - testReal: TypeField.array, - testArrayNull: TypeField.array, - isActive: TypeField.boolean, - createdAt: TypeField.date, - testDate: TypeField.date, - updatedAt: TypeField.date, - }, - roles: { - id: TypeField.number, - name: TypeField.string, - key: TypeField.string, - isDefault: TypeField.boolean, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - comments: { - id: TypeField.number, - text: TypeField.string, - kind: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - notes: { - id: TypeField.string, - text: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - }, - userGroup: { id: TypeField.number, label: TypeField.string }, - }; - - const filterQuerySchema = zodFilterQuerySchema( - fields, - relation, - { - target: usersPropsArray, - roles: rolesPropsArray, - userGroup: userGroupPropsArray, - comments: commentsPropsArray, - notes: notesPropsArray, - manager: usersPropsArray, - addresses: addressesPropsArray, - }, - propsType - ); - type FilterQuerySchema = z.infer>; - - it('Valid schema', () => { - const check1: FilterQuerySchema = { - target: { - id: { - gte: '1213', - ne: '12', - }, - }, - relation: null, - }; - const check2: FilterQuerySchema = { - target: { - id: { - gte: '1213', - }, - login: { - lt: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['sdfsdf', 'sdfsdf'], - }, - }, - }, - }; - const check3: FilterQuerySchema = { - target: null, - relation: null, - }; - const check4: FilterQuerySchema = { - target: null, - relation: { - comments: { - id: { - lte: '123', - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check5: FilterQuerySchema = { - target: null, - relation: { - comments: { - id: { - in: ['1'], - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check6: FilterQuerySchema = { - target: { - id: { - gte: '1213', - ne: '123', - }, - addresses: { - eq: 'null', - }, - }, - relation: null, - }; - const check7: FilterQuerySchema = { - target: { - isActive: { - eq: 'true', - }, - }, - relation: null, - }; - const check8: FilterQuerySchema = { - target: { - createdAt: { - eq: '2023-12-08T09:40:58.020Z', - }, - }, - relation: null, - }; - const check9: FilterQuerySchema = { - target: { - createdAt: { - eq: 'null', - }, - }, - relation: null, - }; - - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - ]; - for (const check of checkArray) { - const result = filterQuerySchema.parse(check); - expect(result).toEqual(check); - } - const result = filterQuerySchema.parse(check9); - expect(result.target!.createdAt!.eq).toEqual(null); - result.target!.createdAt!.eq = 'null'; - expect(result).toEqual(check9); - }); - - it('Invalid schema', () => { - const check1 = null; - const check2 = {}; - const check3 = ''; - const check4 = 1; - const check5: any[] = []; - const check6 = { - target: null, - }; - const check7 = { - target: null, - relation: { - commentsasda: { - id: { - lte: 'sdfsdf', - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check8 = { - target: null, - relation: { - comment: { - id: { - lte: 'sdfsdf', - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - sdfsdf: {}, - }, - }; - const check9 = { - target: { - id: { - gte: '1213', - }, - createdAt: { - lt: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - eq: '1', - }, - }, - }, - }; - const check10 = { - target: { - id: '', - createdAt: { - lt: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['sdfsdf', 'sdfsdf'], - }, - }, - }, - }; - const check11 = { - target: { - createdAt: { - lt1: 'sdfs', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['sdfsdf', 'sdfsdf'], - }, - }, - }, - }; - const check12 = { - target: { - createdAt: { - in: 'sdfs', - }, - }, - relation: null, - }; - const check13 = { - target: { - sdfsdf: { - eq: 'sdfs', - }, - }, - relation: null, - }; - const check14 = { - target: { - sdfsdf: { - eq: 'sdfs', - }, - }, - relation: { - addresses: { - sdfsdf: { - eq: 'dsfsdf', - }, - }, - }, - }; - const check15 = { - target: null, - relation: { - addresses: {}, - }, - }; - const check16 = { - target: { - id: { - gte: '1213', - ne: 'sdfsfdsf', - }, - addresses: { - eqa: 'null', - }, - }, - relation: null, - }; - const check17 = { - target: { - id: { - gte: '1213', - ne: 'sdfsfdsf', - }, - addresses: { - eq: 'sdfsdf', - }, - }, - relation: null, - }; - const check18 = { - target: { - id: { - gte: 'invalidType', - ne: 'sdfsfdsf', - }, - addresses: { - eq: 'sdfsdf', - }, - }, - relation: null, - }; - const check19 = { - target: null, - relation: { - comments: { - id: { - in: ['dsf'], - }, - }, - manager: { - firstName: { - eq: 'sdfsdfsdf', - }, - }, - }, - }; - const check20 = { - target: { - isActive: { - eq: 'sdfsdf', - }, - }, - }; - const check21: FilterQuerySchema = { - target: { - createdAt: { - eq: 'sdfasd', - }, - }, - relation: null, - }; - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - check17, - check18, - check19, - check20, - check21, - ]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - filterQuerySchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts deleted file mode 100644 index 99f9c7cb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/filter.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { - z, - ZodArray, - ZodEffects, - ZodLiteral, - ZodNullable, - ZodObject, - ZodOptional, - ZodString, - ZodUnion, -} from 'zod'; -import { - arrayItemStringLongerThan, - elementOfArrayMustBe, - nonEmptyObject, - oneOf, - stringLongerThan, - stringMustBe, -} from '../zod-utils'; -import { - CastProps, - Entity, - EntityProps, - FilterOperand, - IsArray, - TypeCast, - TypeOfArray, - ValueOf, -} from '../../../types'; -import { ObjectTyped } from '../../utils'; -import { - AllFieldWithTpe, - ArrayPropsForEntity, - guardIsKeyOfObject, - PropsArray, - RelationTree, - ResultGetField, - TypeField, -} from '../../orm'; - -import { - zodFilterRelationSchema, - ZodFilterRelationSchema, -} from '../zod-input-query-schema/filter'; - -type ZodForString = ZodUnion< - [ZodEffects, null, 'null'>, ZodEffects] ->; - -const zodForString: ZodForString = z.union([ - z.literal('null').transform(() => null), - z.string().refine(stringLongerThan(), { - message: 'String should be not empty', - }), -]); - -type ZodForStringArray = ZodEffects< - ZodArray, - [string | null, ...(string | null)[]], - [string, ...string[]] ->; -const zodForStringArray: ZodForStringArray = zodForString - .array() - .nonempty() - .refine(arrayItemStringLongerThan(0), { - message: 'Array should be not empty', - }); - -type OperandForString = Exclude< - ValueOf, - FilterOperand.in | FilterOperand.nin | FilterOperand.some ->; -type OperandForArray = Extract< - ValueOf, - FilterOperand.in | FilterOperand.nin ->; - -type OperandForArrayField = FilterOperand.some; - -type MapOperandForString = { - [K in OperandForString]: ZodOptional; -}; - -type MapOperandForArrayString = { - [K in OperandForArray]: ZodOptional; -}; - -type MapOperand = MapOperandForString & MapOperandForArrayString; -type ZodOperand = ZodEffects>; -type ZodOperandForArrayField = ZodObject< - { - [K in OperandForArrayField]: ZodForStringArray; - }, - 'strict' ->; - -const arrayOperand: { [K in OperandForArray]: boolean } = { - [FilterOperand.nin]: true, - [FilterOperand.in]: true, -}; - -const shapeMapOperand = (type: TypeField = TypeField.string): ZodOperand => - z - .object( - ObjectTyped.entries(FilterOperand) - .filter(([key]) => key !== FilterOperand.some) - .reduce( - (acum, [key, val]) => ({ - ...acum, - [val]: (Object.prototype.hasOwnProperty.call(arrayOperand, key) - ? zodForStringArray.refine(elementOfArrayMustBe(type), { - message: `String should be as ${type}`, - }) - : zodForString.refine(stringMustBe(type), { - message: `String should be as ${type}`, - }) - ).optional(), - }), - {} as MapOperand - ) - ) - .strict() - .refine( - oneOf( - Object.values(FilterOperand).filter((i) => i !== FilterOperand.some) - ), - { - message: `Must have one of: "${Object.values(FilterOperand) - .filter((i) => i !== FilterOperand.some) - .join('","')}"`, - } - ); - -const shapeForArrayField: ZodOperandForArrayField = z - .object({ [FilterOperand.some]: zodForStringArray }) - .strict(); - -type FilterProps = { - [Props in P[number]]: Props extends keyof E - ? IsArray extends true - ? ZodOptional - : ZodOptional - : never; -}; - -type FilterTargetRelation

= { - [Props in P[number]]: ZodOptional; -}; - -const getFilterProps = ( - field: ResultGetField['field'], - propsArray: PropsArray, - propsType: AllFieldWithTpe -): FilterProps['field']> => - field.reduce( - (acum, item) => ({ - ...acum, - [item]: (Reflect.get(propsArray, item) - ? shapeForArrayField - : shapeMapOperand(propsType[item as EntityProps]) - ).optional(), - }), - {} as FilterProps['field']> - ); - -type TargetRelation = FilterTargetRelation< - ResultGetField['relations'] ->; -type TargetProps = FilterProps< - E, - ResultGetField['field'] -> & - TargetRelation; - -type Target = { - target: ZodNullable, 'strict'>>>; -}; - -type RelationType = TypeCast< - TypeOfArray>, - Entity ->; - -type RelationFilter = ZodOptional< - ZodEffects['field']>, 'strict'>> ->; - -type Relation = { - [R in keyof RelationTree]: RelationFilter>; -}; - -type ZodObjectRelation = ZodNullable< - ZodEffects, 'strict'>> ->; - -type ShapeFilter = Target & { - relation: ZodObjectRelation; -}; - -export type ZodFilterQuerySchema = ZodEffects< - ZodObject> ->; - -export const zodFilterQuerySchema = ( - field: ResultGetField['field'], - relationTree: RelationTree, - propsArray: ArrayPropsForEntity, - propsType: AllFieldWithTpe -): ZodFilterQuerySchema => { - const { target: propsArrayTarget, ...relationPropsArray } = propsArray; - - const targetShape: FilterProps['field']> = - getFilterProps(field, propsArrayTarget, propsType); - - const targetRelation = ObjectTyped.keys(relationTree).reduce( - (acum, item) => ({ - ...acum, - [item]: zodFilterRelationSchema.optional(), - }), - {} as TargetRelation - ); - - const targetProps: TargetProps = { - ...targetShape, - ...targetRelation, - }; - - const target: Target = { - target: z.object(targetProps).strict().refine(nonEmptyObject()).nullable(), - }; - - const relationPlaceHolder = ObjectTyped.keys(relationTree).reduce( - (acum, item) => { - acum[item] = undefined; - return acum; - }, - {} as { [K in keyof RelationTree]: undefined } - ); - const relation = ObjectTyped.keys(relationTree).reduce((acum, name) => { - type F = typeof name; - type RT = RelationType; - type RTF = ResultGetField['field']; - - const relationField = relationTree[name] as RTF; - guardIsKeyOfObject(relationPropsArray, name); - - const propsArrayForRelation = relationPropsArray[name] as PropsArray; - - const filterProps = getFilterProps( - relationField, - propsArrayForRelation, - propsType[name] as AllFieldWithTpe - ); - const zodFilter: RelationFilter = z - .object(filterProps) - .strict() - .refine(nonEmptyObject()) - .optional(); - const newName: any = name; - guardIsKeyOfObject(relationPlaceHolder, newName); - - acum[newName] = zodFilter; - return acum; - }, {} as Relation); - - const zodObjectRelation: ZodObjectRelation = z - .object(relation) - .strict() - .refine(nonEmptyObject()) - .nullable(); - - const shapeFilter: ShapeFilter = { - ...target, - ['relation']: zodObjectRelation, - }; - - return z.object(shapeFilter).strict().refine(nonEmptyObject()); -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts deleted file mode 100644 index 19f6e82b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { zodIncludeQuerySchema } from './include'; -import { ZodError } from 'zod'; - -describe('Check "include" zod schema', () => { - const relations = [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', - ] as const; - const zodIncludeQuerySchemaResult = zodIncludeQuerySchema(relations); - - it('Valid schema', () => { - const check1 = ['addresses']; - const result1 = zodIncludeQuerySchemaResult.parse(check1); - expect(result1).toEqual(check1); - - const check2 = ['addresses', 'manager']; - const result2 = zodIncludeQuerySchemaResult.parse(check2); - expect(result2).toEqual(check2); - }); - - it('Invalid schema', () => { - const check1: string[] = []; - const check2: string[] = ['test']; - const check3: string[] = ['addresses', 'manager', 'manager']; - - const checkArray = [check1, check2, check3]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - zodIncludeQuerySchemaResult.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts deleted file mode 100644 index eedc60e6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/include.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Writeable, z, ZodArray, ZodEffects, ZodEnum } from 'zod'; -import { uniqueArray } from '../zod-utils'; - -export type ZodIncludeQuerySchema = - ZodEffects< - ZodArray>, 'atleastone'>, - [Writeable[number], ...Writeable[number][]], - [Writeable[number], ...Writeable[number][]] - >; - -export const zodIncludeQuerySchema = ( - relationList: U -): ZodIncludeQuerySchema => - z.enum(relationList).array().nonempty().refine(uniqueArray(), { - message: 'Include should have unique relation', - }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts deleted file mode 100644 index 7102e7cb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/index.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { zodFilterQuerySchema, ZodFilterQuerySchema } from './filter'; -import { - zodSelectFieldsQuerySchema, - ZodSelectFieldsQuerySchema, -} from './select'; -import { zodIncludeQuerySchema, ZodIncludeQuerySchema } from './include'; -import { zodSortQuerySchema, ZodSortQuerySchema } from './sort'; -import { zodPageQuerySchema, ZodPageQuerySchema } from './page'; -import { QueryField } from '../zod-helper'; -import { Entity } from '../../../types'; -import { ResultGetField } from '../../orm'; -import { ZodNullable } from 'zod'; - -export { - zodPageQuerySchema, - ZodPageQuerySchema, - zodIncludeQuerySchema, - ZodIncludeQuerySchema, - zodFilterQuerySchema, - ZodFilterQuerySchema, - zodSelectFieldsQuerySchema, - ZodSelectFieldsQuerySchema, - zodSortQuerySchema, - ZodSortQuerySchema, -}; - -export type ZodQueryShape = { - [QueryField.filter]: ZodFilterQuerySchema; - [QueryField.fields]: ZodNullable>; - [QueryField.include]: ZodNullable< - ZodIncludeQuerySchema['relations']> - >; - [QueryField.sort]: ZodNullable>; - [QueryField.page]: ZodPageQuerySchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts deleted file mode 100644 index 36a69cba..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { zodPageQuerySchema } from './page'; -import { ZodError } from 'zod'; -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../constants'; - -describe('Check "page" zod schema', () => { - it('Valid schema', () => { - const defaultPage = { size: DEFAULT_PAGE_SIZE, number: DEFAULT_QUERY_PAGE }; - const check1 = { size: 1, number: 1 }; - const check2 = { size: 1 }; - const check3 = undefined; - const check4 = { number: 1 }; - const check5 = {}; - const check6 = { size: '1', number: '1' }; - const result1 = zodPageQuerySchema.parse(check1); - const result2 = zodPageQuerySchema.parse(check2); - const result3 = zodPageQuerySchema.parse(check3); - const result4 = zodPageQuerySchema.parse(check4); - const result5 = zodPageQuerySchema.parse(check5); - const result6 = zodPageQuerySchema.parse(check6); - expect(result1).toEqual(check1); - expect(result2).toEqual({ - ...defaultPage, - ...check2, - }); - expect(result3).toEqual(defaultPage); - expect(result4).toEqual({ - ...defaultPage, - ...check4, - }); - expect(result5).toEqual(defaultPage); - expect(result6).toEqual(check1); - }); - - it('Invalid schema', () => { - const check1 = { size: 0 }; - const check2 = { size: -1 }; - const check3 = { size: 'sdfsdf' }; - const check4 = { size: -1, number: '21ad' }; - const check5 = { size: -1, number: -1 }; - const check6 = { size: 'sdsad', number: '21ad' }; - const check7 = { size: 1, number: 2, otherProps: 'dsfsdf' }; - const check8 = { size: 1, otherProps: 'dsfsdf' }; - - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - ]; - - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - zodPageQuerySchema.parse(check1); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts deleted file mode 100644 index 3a35c0bf..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/page.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { z, ZodDefault, ZodEffects, ZodNumber, ZodObject } from 'zod'; -import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../constants'; - -export type ZodPageQuerySchema = ZodDefault< - ZodObject< - { - size: ZodEffects, number, unknown>; - number: ZodEffects, number, unknown>; - }, - 'strict' - > ->; - -const checkNumber = (a: unknown) => { - if (typeof a === 'string') { - return parseInt(a, 10); - } else if (typeof a === 'number') { - return a; - } else { - return undefined; - } -}; -export const zodPageQuerySchema: ZodPageQuerySchema = z - .object({ - size: z.preprocess( - checkNumber, - z.number().int().min(1).default(DEFAULT_PAGE_SIZE) - ), - number: z.preprocess( - checkNumber, - z.number().int().min(1).default(DEFAULT_QUERY_PAGE) - ), - }) - .strict() - .default({ - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts deleted file mode 100644 index e493afe3..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { RelationTree, ResultGetField } from '../../orm'; -import { Users } from '../../../mock-utils'; -import { - ZodSelectFieldsQuerySchema, - zodSelectFieldsQuerySchema, -} from './select'; -import { z, ZodError } from 'zod'; - -describe('Check "select" zod schema', () => { - const relation: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], - }; - const fields: ResultGetField['field'] = [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ]; - - const selectQuerySchema = zodSelectFieldsQuerySchema(fields, relation); - type SelectTypeQuery = z.infer>; - - it('Valid schema', () => { - const check1: SelectTypeQuery = { - target: ['id', 'createdAt', 'isActive'], - userGroup: ['label'], - roles: ['createdAt'], - }; - const check2: SelectTypeQuery = { - userGroup: ['label'], - roles: ['createdAt'], - }; - const check3: SelectTypeQuery = { - addresses: ['city'], - }; - const checkArray = [check1, check2, check3]; - for (const check of checkArray) { - const result = selectQuerySchema.parse(check); - expect(result).toEqual(check); - } - }); - - it('Invalid schema', () => { - const check1 = {}; - const check2 = null; - const check3 = ''; - const check4: [] = []; - const check5 = { - target: ['id', 'createdAt', 'isActive'], - userGroup1: ['label'], - roles: ['createdAt', 'createdAt'], - }; - const check6 = { - target1: ['id', 'createdAt', 'isActive'], - userGroup: ['label'], - roles: ['createdAt', 'createdAt'], - }; - const check7 = { - target: ['id1', 'createdAt', 'isActive'], - userGroup: ['label'], - }; - const check8 = { - target: ['id1', 'createdAt', 'isActive'], - userGroup: ['label'], - roles: ['createdAt1', 'createdAt'], - }; - const check9 = { - target: '', - userGroup: ['label'], - }; - const check10 = { - target: {}, - userGroup: ['label'], - }; - const check11 = { - target: [], - userGroup: ['label'], - }; - const check12 = { - target: '', - userGroup: ['label'], - }; - const check13 = { - target: null, - userGroup: ['label'], - }; - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - ]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - selectQuerySchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts deleted file mode 100644 index ca3690be..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/select.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - Writeable, - z, - ZodArray, - ZodEffects, - ZodEnum, - ZodObject, - ZodOptional, -} from 'zod'; - -import { Entity } from '../../../types'; -import { RelationTree, ResultGetField } from '../../orm'; -import { ObjectTyped } from '../../utils'; -import { nonEmptyObject, uniqueArray } from '../zod-utils'; - -type ZodField = ZodEffects< - ZodArray>, 'atleastone'>, - [Writeable[number], ...Writeable[number][]], - [Writeable[number], ...Writeable[number][]] ->; - -const zodFields = ( - fields: R -): ZodField => - z.enum(fields).array().nonempty().refine(uniqueArray(), { - message: 'Field should be unique', - }); - -type ZodTarget = { - target: ZodOptional>; -}; - -export const zodTarget = ( - fields: R -): ZodTarget => ({ - target: zodFields([...fields]).optional(), -}); - -type ZodRelation = { - [K in keyof RelationTree]: ZodOptional[K]>>; -}; - -type ZodResultShape = ZodTarget['field']> & - ZodRelation; - -export type ZodSelectFields = ZodEffects< - ZodObject, 'strict'> ->; - -export type ZodSelectFieldsQuerySchema = ZodEffects< - ZodSelectFields ->; -export const zodSelectFieldsQuerySchema = ( - fields: ResultGetField['field'], - relationList: RelationTree -): ZodSelectFieldsQuerySchema => { - const target: ZodTarget['field']> = zodTarget(fields); - - const relation = ObjectTyped.entries(relationList).reduce( - (acum, [key, val]) => { - acum[key] = zodFields(val).optional(); - return acum; - }, - {} as ZodRelation - ); - - const resultShape: ZodResultShape = { - ...target, - ...relation, - }; - const zodSelectFields = z - .object(resultShape) - .strict('Should be only target of relation '); - // @ts-ignore - return zodSelectFields.refine(nonEmptyObject(), { - message: 'Validation error: Select target or relation fields', - }) as unknown as ZodSelectFieldsQuerySchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts deleted file mode 100644 index cfb82568..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.spec.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { z, ZodError } from 'zod'; - -import { zodSortQuerySchema, ZodSortQuerySchema } from './sort'; -import { RelationTree, ResultGetField } from '../../orm'; -import { Users } from '../../../mock-utils'; - -describe('Check "sort" zod schema', () => { - const relation: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], - }; - const fields: ResultGetField['field'] = [ - 'testDate', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'updatedAt', - 'createdAt', - 'id', - ]; - - const sortQuerySchema = zodSortQuerySchema(fields, relation); - type SortTypeQuery = z.infer>; - it('Valid schema', () => { - const check1: SortTypeQuery = { - target: { - id: 'DESC', - createdAt: 'ASC', - }, - }; - const check2: SortTypeQuery = { - comments: { - kind: 'ASC', - createdAt: 'ASC', - }, - roles: { - id: 'ASC', - }, - }; - const checkArray = [check1, check2]; - for (const check of checkArray) { - const result = sortQuerySchema.parse(check); - expect(result).toEqual(check); - } - }); - it('Invalid schema', () => { - const check1 = ''; - const check2 = null; - const check3: string[] = ['test']; - const check4: string[] = ['addresses', 'manager', 'manager']; - const check5 = {}; - const check6 = { - sdfsf: null, - }; - const check7 = { - sdfsf: null, - sfdsdf: null, - }; - - const check8 = { - comments: {}, - }; - const check9 = { - comments: null, - }; - const check10 = { - comments: 's', - }; - const check11 = { - comments: [], - }; - const check12 = { - comments: { - kindsdfsdf: 'ASC', - }, - }; - const check13 = { - comments: { - kind: '', - }, - }; - const check14 = { - comments: { - kind: 'sdfsdf', - }, - }; - const check15 = { - comments: { - kind: [], - }, - }; - const check16 = { - comments: { - kind: {}, - }, - }; - const check17 = { - target: {}, - }; - const check18 = { - target: null, - }; - const check19 = { - target: 's', - }; - const check20 = { - target: [], - }; - const check21 = { - target: { - idsdfsdf: 'ASC', - }, - }; - const check22 = { - target: { - id: '', - }, - }; - const check23 = { - target: { - id: 'sdfsdf', - }, - }; - const check24 = { - target: { - id: [], - }, - }; - const check25 = { - target: { - id: {}, - }, - }; - - const checkArray = [ - check1, - check2, - check3, - check4, - check5, - check6, - check7, - check8, - check9, - check10, - check11, - check12, - check13, - check14, - check15, - check16, - check17, - check18, - check19, - check20, - check21, - check22, - check23, - check24, - check25, - ]; - expect.assertions(checkArray.length); - for (const check of checkArray) { - try { - sortQuerySchema.parse(check); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts deleted file mode 100644 index a2d89c06..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-query-schema/sort.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { Entity } from '../../../types'; -import { z, ZodEffects, ZodEnum, ZodObject, ZodOptional } from 'zod'; -import { RelationTree, ResultGetField } from '../../orm'; -import { SORT_TYPE } from '../../../constants'; -import { nonEmptyObject } from '../zod-utils'; - -import { ObjectTyped } from '../../utils'; - -export type SortTarget = { - target: SortEntityOptionalObject; -}; - -export const sortTarget = ( - fields: R -): SortTarget => ({ - target: z.object(sortEntity(fields)).strict().refine(nonEmptyObject()), -}); - -export const sortEntity = ( - fields: R -): SortEntityOptional => - fields.reduce( - (acum, item) => ({ - ...acum, - ...{ - [item]: z.enum(SORT_TYPE).optional(), - }, - }), - {} as SortEntityOptional - ); - -export type SortEnum = ZodEnum<['DESC', 'ASC']>; - -export type SortEntityOptional = { - [K in F[number]]: ZodOptional; -}; - -export type SortEntityOptionalObject = - ZodEffects, 'strict'>>; - -export type SortRelation = { - [k in keyof RelationTree]: SortEntityOptionalObject[k]>; -}; - -export type Sort = SortTarget['field']> & - SortRelation; - -export type OptionalSort = { - [k in keyof Sort]: ZodOptional[k]>; -}; - -type ZodBaseObjectSchema = ZodObject, 'strict'>; -type OptionalZodBaseObjectSchema = ZodObject< - OptionalSort, - 'strict' ->; -export type ZodSortQuerySchema = ZodEffects< - OptionalZodBaseObjectSchema ->; - -export const zodSortQuerySchema = ( - fields: ResultGetField['field'], - relation: RelationTree -): ZodSortQuerySchema => { - const sortTargetObject = sortTarget(fields); - - const sortRelationObject = ObjectTyped.entries(relation).reduce( - (acum, [item, fields]) => { - return { - ...acum, - ...{ - [item]: z - .object(sortEntity(fields)) - .strict() - .refine(nonEmptyObject()), - }, - }; - }, - {} as SortRelation - ); - - const sortMerge: Sort = { - ...sortTargetObject, - ...sortRelationObject, - }; - - const baseZodSchema: ZodBaseObjectSchema = z.object(sortMerge).strict(); - - const partialZod = baseZodSchema.partial() as OptionalZodBaseObjectSchema; - - return partialZod.refine( - nonEmptyObject() - ) as unknown as ZodSortQuerySchema; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts deleted file mode 100644 index a9bc2bb6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { - oneOf, - stringLongerThan, - arrayItemStringLongerThan, - uniqueArray, - nonEmptyObject, -} from './zod-utils'; - -describe('zod-utils', () => { - let checkFunk: (arg: any) => boolean; - describe('oneOf', () => { - beforeAll(() => { - checkFunk = oneOf(['props', 'props1', 'props2']); - }); - - it('Should be ok', () => { - const result1 = checkFunk({ props: 1 }); - const result2 = checkFunk({ props: 1, other: 1 }); - const result3 = checkFunk({ props2: 1, other: 1 }); - expect(result1).toBe(true); - expect(result2).toBe(true); - expect(result3).toBe(true); - }); - - it('Should be not ok', () => { - const result1 = checkFunk({}); - const result2 = checkFunk({ other: 1 }); - const result3 = checkFunk({ props4: 1, other: 1 }); - expect(result1).toBe(false); - expect(result2).toBe(false); - expect(result3).toBe(false); - }); - }); - - describe('stringLongerThan', () => { - beforeAll(() => { - checkFunk = stringLongerThan(5); - }); - it('Should be ok', () => { - const result = checkFunk('123456'); - expect(result).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk('12345'); - const result2 = checkFunk('1234'); - expect(result).toBe(false); - expect(result2).toBe(false); - }); - }); - - describe('arrayItemStringLongerThan', () => { - beforeAll(() => { - checkFunk = arrayItemStringLongerThan(5); - }); - it('Should be ok', () => { - const result = checkFunk(['123456', '123456']); - expect(result).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk(['12345', '123456']); - const result2 = checkFunk(['1234', '123456']); - const result3 = checkFunk(['123456', '1234']); - expect(result).toBe(false); - expect(result2).toBe(false); - expect(result3).toBe(false); - }); - }); - - describe('uniqueArray', () => { - beforeAll(() => { - checkFunk = uniqueArray(); - }); - it('Should be ok', () => { - const result = checkFunk(['1', '2', '3', '4']); - const result2 = checkFunk([1, 2, 3, 4]); - expect(result).toBe(true); - expect(result2).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk(['1', '1', '123456']); - const result2 = checkFunk(['1', '123456', '1']); - const result3 = checkFunk([1, '1234', 1]); - expect(result).toBe(false); - expect(result2).toBe(false); - expect(result3).toBe(false); - }); - }); - - describe('nonEmptyObject', () => { - beforeAll(() => { - checkFunk = nonEmptyObject(); - }); - it('Should be ok', () => { - const result = checkFunk({ test: 1 }); - expect(result).toBe(true); - }); - - it('Should be not ok', () => { - const result = checkFunk({}); - const result2 = checkFunk(undefined); - expect(result).toBe(false); - expect(result2).toBe(false); - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts b/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts deleted file mode 100644 index f265e25b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/helper/zod/zod-utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { TypeField } from '../orm'; - -export const oneOf = (keys: string[]) => (val: any) => { - for (const k of keys) { - if (val[k] !== undefined) return true; - } - return false; -}; - -export const stringLongerThan = - (length = 0) => - (str: string) => - str.length > length; - -export const arrayItemStringLongerThan = - (length = 0) => - (array: [string | null, ...(string | null)[]]) => { - const checkFunction = stringLongerThan(length); - return !array.some((i) => i !== null && !checkFunction(i)); - }; - -export const stringMustBe = - (type: TypeField = TypeField.string) => - (inputString: string | null) => { - if (inputString === null) return true; - switch (type) { - case TypeField.boolean: - return inputString === 'true' || inputString === 'false'; - case TypeField.number: - return !isNaN(+inputString); - case TypeField.date: - return new Date(inputString).toString() !== 'Invalid Date'; - default: - return true; - } - }; -export const elementOfArrayMustBe = - (type: TypeField = TypeField.string) => - (inputArray: unknown[]) => { - const checkFunc = stringMustBe(type); - return !inputArray.some((i) => !checkFunc(`${i}`)); - }; -export const uniqueArray = () => (e: string[]) => new Set(e).size === e.length; - -export const nonEmptyObject = - () => - (object: T) => - !!(object && Object.keys(object).length > 0); - -export const getValidationErrorForStrict = ( - props: string[], - name: 'Fields' | 'Filter' -) => - `Validation error: ${name} should be have only props: ["${props.join( - '","' - )}"]`; diff --git a/libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts b/libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts deleted file mode 100644 index 641fb5c3..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/json-api-nestjs-common.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { DynamicModule, Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { ModuleOptions } from './types'; -import { GLOBAL_MODULE_OPTIONS_TOKEN } from './constants'; -import { CurrentDataSourceProvider, SwaggerBindMethod } from './factory'; -import { TransformInputService, EntityPropsMapService } from './service'; - -@Module({}) -export class JsonApiNestJsCommonModule { - static forRoot(options: ModuleOptions): DynamicModule { - const optionProvider = { - provide: GLOBAL_MODULE_OPTIONS_TOKEN, - useValue: options, - }; - - const currentDataSourceProvider = CurrentDataSourceProvider( - options.connectionName - ); - - const typeOrmModule = TypeOrmModule.forFeature( - options.entities, - options.connectionName - ); - - return { - module: JsonApiNestJsCommonModule, - imports: [typeOrmModule, ...(options.imports || [])], - providers: [ - ...(options.providers || []), - optionProvider, - currentDataSourceProvider, - TransformInputService, - EntityPropsMapService, - SwaggerBindMethod, - ], - exports: [ - ...(options.providers || []), - typeOrmModule, - optionProvider, - currentDataSourceProvider, - TransformInputService, - EntityPropsMapService, - SwaggerBindMethod, - ...(options.imports || []), - ], - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts deleted file mode 100644 index 9f54a008..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { DynamicModule, Module } from '@nestjs/common'; -import { DiscoveryModule, RouterModule } from '@nestjs/core'; -import { ConfigParamDefault, DEFAULT_CONNECTION_NAME } from './constants'; -import { ModuleOptions } from './types'; -import { JsonApiNestJsCommonModule } from './json-api-nestjs-common.module'; -import { JSON_API_DECORATOR_ENTITY } from './constants'; -import { BaseModuleClass } from './mixin'; -import { AtomicOperationModule } from './modules/atomic-operation/atomic-operation.module'; - -@Module({}) -export class JsonApiModule { - private static connectionName = DEFAULT_CONNECTION_NAME; - - public static forRoot(options: ModuleOptions): DynamicModule { - JsonApiModule.connectionName = - options.connectionName || JsonApiModule.connectionName; - - options.connectionName = JsonApiModule.connectionName; - options.options = { - ...ConfigParamDefault, - ...options.options, - }; - - const commonModule = JsonApiNestJsCommonModule.forRoot(options); - - const entityImport = options.entities.map((entity) => { - const controller = (options.controllers || []).find( - (item) => - item && - Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, item) === entity - ); - const module = BaseModuleClass.forRoot({ - entity, - connectionName: JsonApiModule.connectionName, - controller, - config: { - ...ConfigParamDefault, - ...options.options, - }, - }); - module.imports = [ - DiscoveryModule, - commonModule, - ...(module.imports || []), - ]; - return module; - }); - - const operationModuleImport = options.options?.operationUrl - ? [ - AtomicOperationModule.forRoot( - { - ...options, - connectionName: JsonApiModule.connectionName, - }, - entityImport, - commonModule - ), - RouterModule.register([ - { - module: AtomicOperationModule, - path: options.options.operationUrl, - }, - ]), - ] - : []; - - return { - module: JsonApiModule, - imports: [...operationModuleImport, ...entityImport], - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts deleted file mode 100644 index 0581ba74..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/controller/json-base.controller.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - MethodName, - Entity, - TypeormService, - EntityRelation, - ResourceObject, - ResourceObjectRelationships, -} from '../../types'; -import { - PostData, - Query, - PatchData, - PostRelationshipData, - PatchRelationshipData, -} from '../../helper'; -import { TYPEORM_SERVICE_PROPS } from '../../constants'; - -type RequestMethodeObject = { [k in MethodName]: (...arg: any[]) => any }; - -interface IJsonBaseController extends RequestMethodeObject {} - -export class JsonBaseController - implements IJsonBaseController -{ - private [TYPEORM_SERVICE_PROPS]!: TypeormService; - - getOne(id: string | number, query: Query): Promise> { - return this[TYPEORM_SERVICE_PROPS].getOne(id, query); - } - getAll(query: Query): Promise> { - return this[TYPEORM_SERVICE_PROPS].getAll(query); - } - deleteOne(id: string | number): Promise { - return this[TYPEORM_SERVICE_PROPS].deleteOne(id); - } - - patchOne( - id: string | number, - inputData: PatchData - ): Promise> { - return this[TYPEORM_SERVICE_PROPS].patchOne(id, inputData); - } - - postOne(inputData: PostData): Promise> { - return this[TYPEORM_SERVICE_PROPS].postOne(inputData); - } - - getRelationship>( - id: string | number, - relName: Rel - ): Promise> { - return this[TYPEORM_SERVICE_PROPS].getRelationship(id, relName); - } - postRelationship>( - id: string | number, - relName: Rel, - input: PostRelationshipData - ): Promise> { - return this[TYPEORM_SERVICE_PROPS].postRelationship(id, relName, input); - } - - deleteRelationship>( - id: string | number, - relName: Rel, - input: PostRelationshipData - ): Promise { - return this[TYPEORM_SERVICE_PROPS].deleteRelationship(id, relName, input); - } - - patchRelationship>( - id: string | number, - relName: Rel, - input: PatchRelationshipData - ): Promise> { - return this[TYPEORM_SERVICE_PROPS].patchRelationship(id, relName, input); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/index.ts deleted file mode 100644 index b71f2177..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './module/module.mixin'; -export * from './controller/json-base.controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts deleted file mode 100644 index 53f35403..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/error.interceptors.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { - InternalServerErrorException, - CallHandler, - ExecutionContext, - Inject, - Injectable, - NestInterceptor, -} from '@nestjs/common'; -import { catchError, Observable, throwError } from 'rxjs'; -import { QueryFailedError, Repository } from 'typeorm'; -import { DriverUtils } from 'typeorm/driver/DriverUtils'; - -import { - CONTROL_OPTIONS_TOKEN, - CURRENT_ENTITY_REPOSITORY, -} from '../../constants'; -import { ConfigParam, Entity, ValidateQueryError } from '../../types'; -import { - MysqlError, - MysqlErrorCode, - PostgresError, - PostgresErrorCode, -} from '../../helper'; -import { HttpException } from '@nestjs/common'; - -@Injectable() -export class ErrorInterceptors implements NestInterceptor { - @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; - @Inject(CONTROL_OPTIONS_TOKEN) private config!: ConfigParam; - - intercept( - context: ExecutionContext, - next: CallHandler - ): Observable | Promise> { - return next.handle().pipe( - catchError((error) => { - if (error instanceof QueryFailedError) { - return throwError(() => this.prepareDataBaseError(error)); - } - - if (error instanceof HttpException) { - return throwError(() => error); - } - - const errorObject: ValidateQueryError = { - code: 'internal_error', - message: this.config.debug ? error.message : 'Internal Server Error', - path: [], - }; - const descriptionOrOptions = this.config.debug ? error : undefined; - return throwError( - () => - new InternalServerErrorException( - [errorObject], - descriptionOrOptions - ) - ); - }) - ); - } - - private prepareDataBaseError(error: QueryFailedError): HttpException { - const errorObject: ValidateQueryError = { - code: 'internal_error', - message: this.config.debug ? error.message : 'Internal Server Error', - path: [], - }; - - if (DriverUtils.isMySQLFamily(this.repository.manager.connection.driver)) { - const { errorCode, errorMsg } = this.prepareMysqlError(error.driverError); - if (MysqlError[errorCode]) { - return MysqlError[errorCode](this.repository.metadata, errorMsg); - } - } - - if ( - DriverUtils.isPostgresFamily(this.repository.manager.connection.driver) - ) { - const { errorCode, errorMsg, detail } = this.preparePostgresError( - error.driverError - ); - - if (PostgresError[errorCode]) { - return PostgresError[errorCode]( - this.repository.metadata, - errorMsg, - detail - ); - } - } - - return new InternalServerErrorException([errorObject]); - } - - private prepareMysqlError(error: any): { - errorCode: MysqlErrorCode; - errorMsg: string; - } { - return { - errorCode: error.errno, - errorMsg: error.message, - }; - } - - private preparePostgresError(error: any): { - errorCode: PostgresErrorCode; - errorMsg: string; - detail: string; - } { - return { - errorCode: error.code, - errorMsg: error.message, - detail: error.detail, - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts deleted file mode 100644 index b6030081..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './error.interceptors'; -export * from './log-time.interceptors'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts deleted file mode 100644 index badabaed..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/interceptors/log-time.interceptors.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; -import { map, Observable } from 'rxjs'; - -import { Entity, ResourceObject } from '../../types'; - -export class LogTimeInterceptors implements NestInterceptor { - intercept( - context: ExecutionContext, - next: CallHandler> - ): Observable | Promise> { - const now = Date.now(); - return next.handle().pipe( - map((r) => { - const response = context.switchToHttp().getResponse(); - const time = Date.now() - now; - response.setHeader('x-response-time', time); - if (r && r.meta) { - r.meta['time'] = time; - } - - return r; - }) - ); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts deleted file mode 100644 index 04225e97..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/module/module.mixin.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { DynamicModule } from '@nestjs/common'; - -import { BaseModuleOptions, ConfigParam, DecoratorOptions } from '../../types'; -import { - createController, - getProviderName, - nameIt, - bindController, -} from '../../helper'; -import { - JSON_API_DECORATOR_OPTIONS, - ConfigParamDefault, - JSON_API_MODULE_POSTFIX, - CONTROL_OPTIONS_TOKEN, -} from '../../constants'; -import { - ZodInputQuerySchema, - ZodQuerySchema, - TypeormServiceFactory, - EntityRepositoryFactory, - ZodInputPostSchema, - ZodInputPatchSchema, - ZodInputPostRelationshipSchema, - ZodInputPatchRelationshipSchema, -} from '../../factory'; -import { TypeormUtilsService } from '../service'; -import { TransformDataService } from '../service'; -import { SwaggerBindService } from '../service/swagger-bind.service'; - -export class BaseModuleClass { - static forRoot(options: BaseModuleOptions): DynamicModule { - const { entity, connectionName, controller } = options; - const controllerClass = createController(entity, controller); - - const decoratorOptions: DecoratorOptions = - Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, controllerClass) || {}; - - const moduleConfig: ConfigParam = { - ...ConfigParamDefault, - ...options.config, - ...decoratorOptions, - }; - - bindController(controllerClass, entity, connectionName, moduleConfig); - - const optionProvider = { - provide: CONTROL_OPTIONS_TOKEN, - useValue: moduleConfig, - }; - - return { - module: nameIt( - getProviderName(entity, JSON_API_MODULE_POSTFIX), - BaseModuleClass - ), - controllers: [controllerClass], - providers: [ - ZodInputQuerySchema(entity), - ZodQuerySchema(entity), - ZodInputPostSchema(entity), - ZodInputPatchSchema(entity), - ZodInputPostRelationshipSchema, - ZodInputPatchRelationshipSchema, - TypeormServiceFactory(entity), - EntityRepositoryFactory(entity), - optionProvider, - TypeormUtilsService, - TransformDataService, - SwaggerBindService, - ], - imports: [], - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts deleted file mode 100644 index 2e84898d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - pullUser, - Users, -} from '../../../mock-utils'; -import { CheckItemEntityPipe } from './check-item-entity.pipe'; -import { TypeormUtilsService } from '../../../service'; - -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../../factory'; -import { - CURRENT_ENTITY_REPOSITORY, - DEFAULT_CONNECTION_NAME, -} from '../../../constants'; -import { Repository } from 'typeorm'; -import { NotFoundException } from '@nestjs/common'; - -describe('CheckItemEntityPipe', () => { - let db: IMemoryDb; - let checkItemEntityPipe: CheckItemEntityPipe; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(Users), - TypeormUtilsService, - CheckItemEntityPipe, - ], - }).compile(); - - await pullUser(module.get>(CURRENT_ENTITY_REPOSITORY)); - - checkItemEntityPipe = - module.get>(CheckItemEntityPipe); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be correct', async () => { - const id = 1; - const result = await checkItemEntityPipe.transform(id); - expect(result).toBe(id); - }); - - it('Should be error', async () => { - expect.assertions(1); - try { - await checkItemEntityPipe.transform(11111); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts deleted file mode 100644 index a42cb10d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/check-item-entity.pipe.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Inject, NotFoundException, PipeTransform } from '@nestjs/common'; -import { Repository } from 'typeorm'; - -import { CURRENT_ENTITY_REPOSITORY } from '../../../constants'; -import { Entity, ValidateQueryError } from '../../../types'; -import { TypeormUtilsService } from '../../service/typeorm-utils.service'; - -export class CheckItemEntityPipe - implements PipeTransform> -{ - @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; - @Inject(TypeormUtilsService) - private typeormUtilsService!: TypeormUtilsService; - async transform(value: I): Promise { - const params = 'params'; - const result = await this.repository - .createQueryBuilder(this.typeormUtilsService.currentAlias) - .where( - `${this.typeormUtilsService.getAliasPath( - this.typeormUtilsService.currentPrimaryColumn - )} = :${params}` - ) - .setParameters({ - [params]: value, - }) - .getOne(); - if (!result) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${value}' does not exist`, - path: [], - }; - throw new NotFoundException([error]); - } - return value; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts deleted file mode 100644 index 256cdb57..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/check-item-entity/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './check-item-entity.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts deleted file mode 100644 index fe926be1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Injectable, ParseIntPipe } from '@nestjs/common'; - -import { QueryPipe } from './query'; -import { QueryInputPipe } from './query-input'; -import { QueryFiledInIncludePipe } from './query-filed-on-include'; -import { QueryCheckSelectField } from './query-check-select-field'; - -import { ConfigParam, Entity, PipeMixin } from '../../types'; -import { nameIt } from '../../helper'; -import { CheckItemEntityPipe } from './check-item-entity'; -import { PostInputPipe } from './post-input'; -import { PatchInputPipe } from './patch-input'; -import { PostRelationshipPipe } from './post-relationship'; -import { ParseRelationshipNamePipe } from './parse-relationship-name'; -import { PatchRelationshipPipe } from './patch-relationship'; - -function factoryMixin(entity: Entity, connectionName: string, pipe: PipeMixin) { - const entityName = - entity instanceof Function ? entity.name : entity['options'].name; - - const pipeClass = nameIt( - `${entityName.charAt(0).toUpperCase() + entityName.slice(1)}${pipe.name}`, - pipe - ) as PipeMixin; - - Injectable()(pipeClass); - - return pipeClass; -} - -export function queryInputMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, QueryInputPipe); -} - -export function queryMixin(entity: Entity, connectionName: string): PipeMixin { - return factoryMixin(entity, connectionName, QueryPipe); -} - -export function queryFiledInIncludeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, QueryFiledInIncludePipe); -} - -export function queryCheckSelectFieldMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, QueryCheckSelectField); -} - -export function idPipeMixin( - entity: Entity, - connectionName: string, - config?: ConfigParam -): PipeMixin { - return config && config.pipeForId ? config.pipeForId : ParseIntPipe; -} - -export function checkItemEntityPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, CheckItemEntityPipe); -} -export function postInputPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PostInputPipe); -} - -export function patchInputPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PatchInputPipe); -} - -export function postRelationshipPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PostRelationshipPipe); -} - -export function patchRelationshipPipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, PatchRelationshipPipe); -} -export function parseRelationshipNamePipeMixin( - entity: Entity, - connectionName: string -): PipeMixin { - return factoryMixin(entity, connectionName, ParseRelationshipNamePipe); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts deleted file mode 100644 index 62c4518d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './parse-relationship-name.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts deleted file mode 100644 index 11fbf094..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; -import { ParseRelationshipNamePipe } from './parse-relationship-name.pipe'; -import { TypeormUtilsService } from '../../service/typeorm-utils.service'; - -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../constants'; -import { UnprocessableEntityException } from '@nestjs/common'; - -describe('ParseRelationshipNamePipe', () => { - let db: IMemoryDb; - let parseRelationshipNamePipe: ParseRelationshipNamePipe; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(Users), - TypeormUtilsService, - ParseRelationshipNamePipe, - ], - }).compile(); - - parseRelationshipNamePipe = module.get>( - ParseRelationshipNamePipe - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be correct', async () => { - const relName = 'userGroup'; - const result = await parseRelationshipNamePipe.transform(relName); - expect(result).toBe(relName); - }); - - it('Should be error', async () => { - expect.assertions(1); - try { - await parseRelationshipNamePipe.transform('11111'); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts deleted file mode 100644 index 3963f03e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - PipeTransform, - UnprocessableEntityException, - Inject, -} from '@nestjs/common'; -import { TypeormUtilsService } from '../../service/typeorm-utils.service'; -import { Entity, EntityRelation, ValidateQueryError } from '../../../types'; - -export class ParseRelationshipNamePipe - implements PipeTransform> -{ - @Inject(TypeormUtilsService) - private typeormUtilsService!: TypeormUtilsService; - - transform(value: string): EntityRelation { - this.checkRelName(value); - return value; - } - - checkRelName(value: unknown): asserts value is EntityRelation { - if (!this.typeormUtilsService.relationFields.find((i) => i === value)) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Relation '${value}' does not exist in resource '${this.typeormUtilsService.currentAlias}'`, - path: [], - }; - throw new UnprocessableEntityException([error]); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts deleted file mode 100644 index 6d849c7a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './patch-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts deleted file mode 100644 index 817ad6bb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { DEFAULT_CONNECTION_NAME, ZOD_PATCH_SCHEMA } from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { PatchInputPipe } from './patch-input.pipe'; -import { ZodInputPostSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PatchInputPipe', () => { - let db: IMemoryDb; - let patchInputPipe: PatchInputPipe; - let zodInputPostSchema: ZodInputPostSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_PATCH_SCHEMA, - useValue: { - parse() {}, - }, - }, - PatchInputPipe, - ], - }).compile(); - - patchInputPipe = module.get>(PatchInputPipe); - zodInputPostSchema = - module.get>(ZOD_PATCH_SCHEMA); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPostSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(patchInputPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts deleted file mode 100644 index ec524211..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-input/patch-input.pipe.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; - -import { Entity, JSONValue } from '../../../types'; -import { PatchData, ZodInputPatchSchema } from '../../../helper/zod'; -import { ZOD_PATCH_SCHEMA } from '../../../constants'; - -export class PatchInputPipe - implements PipeTransform> -{ - @Inject(ZOD_PATCH_SCHEMA) - private zodInputPatchSchema!: ZodInputPatchSchema; - transform(value: JSONValue): PatchData { - try { - return this.zodInputPatchSchema.parse(value, { - errorMap: errorMap, - })['data'] as PatchData; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts deleted file mode 100644 index 99ef16e8..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './patch-relationship.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts deleted file mode 100644 index b5c13c6f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - ZOD_PATCH_RELATIONSHIP_SCHEMA, -} from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, -} from '../../../mock-utils'; - -import { PatchRelationshipPipe } from './patch-relationship.pipe'; -import { ZodInputPostRelationshipSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PatchInputPipe', () => { - let db: IMemoryDb; - let patchRelationshipPipe: PatchRelationshipPipe; - let zodInputPatchRelationshipSchema: ZodInputPostRelationshipSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, - useValue: { - parse() {}, - }, - }, - PatchRelationshipPipe, - ], - }).compile(); - - patchRelationshipPipe = module.get( - PatchRelationshipPipe - ); - zodInputPatchRelationshipSchema = - module.get(ZOD_PATCH_RELATIONSHIP_SCHEMA); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPatchRelationshipSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(patchRelationshipPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest - .spyOn(zodInputPatchRelationshipSchema, 'parse') - .mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - patchRelationshipPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest - .spyOn(zodInputPatchRelationshipSchema, 'parse') - .mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - patchRelationshipPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts deleted file mode 100644 index e200d2f5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/patch-relationship/patch-relationship.pipe.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; - -import { JSONValue } from '../../../types'; -import { - PatchRelationshipData, - ZodInputPatchRelationshipSchema, -} from '../../../helper/zod'; -import { ZOD_PATCH_RELATIONSHIP_SCHEMA } from '../../../constants'; - -export class PatchRelationshipPipe - implements PipeTransform -{ - @Inject(ZOD_PATCH_RELATIONSHIP_SCHEMA) - private zodInputPatchRelationshipSchema!: ZodInputPatchRelationshipSchema; - transform(value: JSONValue): PatchRelationshipData { - try { - return this.zodInputPatchRelationshipSchema.parse(value, { - errorMap: errorMap, - })['data']; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts deleted file mode 100644 index 58efc255..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './post-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts deleted file mode 100644 index c61f5be1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.spec.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { DEFAULT_CONNECTION_NAME, ZOD_POST_SCHEMA } from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { PostInputPipe } from './post-input.pipe'; -import { ZodInputPostSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PostInputPipe', () => { - let db: IMemoryDb; - let postInputPipe: PostInputPipe; - let zodInputPostSchema: ZodInputPostSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_POST_SCHEMA, - useValue: { - parse() {}, - }, - }, - PostInputPipe, - ], - }).compile(); - - postInputPipe = module.get>(PostInputPipe); - zodInputPostSchema = module.get>(ZOD_POST_SCHEMA); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPostSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(postInputPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - postInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodInputPostSchema, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - postInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts deleted file mode 100644 index 2eac0b78..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-input/post-input.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; - -import { Entity, JSONValue } from '../../../types'; -import { PostData, ZodInputPostSchema } from '../../../helper/zod'; -import { ZOD_POST_SCHEMA } from '../../../constants'; - -export class PostInputPipe - implements PipeTransform> -{ - @Inject(ZOD_POST_SCHEMA) private zodInputPostSchema!: ZodInputPostSchema; - transform(value: JSONValue): PostData { - try { - return this.zodInputPostSchema.parse(value, { - errorMap: errorMap, - })['data'] as PostData; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts deleted file mode 100644 index 70457a78..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './post-relationship.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts deleted file mode 100644 index aa1b0c9a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { IMemoryDb } from 'pg-mem'; -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { CurrentDataSourceProvider } from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - ZOD_POST_RELATIONSHIP_SCHEMA, -} from '../../../constants'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, -} from '../../../mock-utils'; - -import { PostRelationshipPipe } from './post-relationship.pipe'; -import { ZodInputPostRelationshipSchema } from '../../../helper/zod'; -import { ZodError } from 'zod'; - -describe('PostInputPipe', () => { - let db: IMemoryDb; - let postRelationshipPipe: PostRelationshipPipe; - let zodInputPostRelationshipSchema: ZodInputPostRelationshipSchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - { - provide: ZOD_POST_RELATIONSHIP_SCHEMA, - useValue: { - parse() {}, - }, - }, - PostRelationshipPipe, - ], - }).compile(); - - postRelationshipPipe = - module.get(PostRelationshipPipe); - zodInputPostRelationshipSchema = module.get( - ZOD_POST_RELATIONSHIP_SCHEMA - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - data, - }; - jest - .spyOn(zodInputPostRelationshipSchema, 'parse') - .mockImplementationOnce(() => check as any); - expect(postRelationshipPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest - .spyOn(zodInputPostRelationshipSchema, 'parse') - .mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - postRelationshipPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest - .spyOn(zodInputPostRelationshipSchema, 'parse') - .mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - postRelationshipPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts deleted file mode 100644 index 73f5a5e4..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/post-relationship/post-relationship.pipe.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; - -import { JSONValue } from '../../../types'; -import { - PostRelationshipData, - ZodInputPostRelationshipSchema, -} from '../../../helper/zod'; -import { ZOD_POST_RELATIONSHIP_SCHEMA } from '../../../constants'; - -export class PostRelationshipPipe - implements PipeTransform -{ - @Inject(ZOD_POST_RELATIONSHIP_SCHEMA) - private zodInputPostRelationshipSchema!: ZodInputPostRelationshipSchema; - transform(value: JSONValue): PostRelationshipData { - try { - return this.zodInputPostRelationshipSchema.parse(value, { - errorMap: errorMap, - })['data']; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts deleted file mode 100644 index 7ba4ed4e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './query-check-select-field'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts deleted file mode 100644 index f2c089d9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; - -import { QueryCheckSelectField } from './query-check-select-field'; -import { Users } from '../../../mock-utils'; -import { CONTROL_OPTIONS_TOKEN } from '../../../constants'; -import { Query, QueryField } from '../../../helper/zod'; -import { ConfigParam, Entity } from '../../../types'; -import { BadRequestException } from '@nestjs/common'; - -function getDefaultQuery() { - const filter = { - relation: null, - target: null, - }; - const defaultQuery: Query = { - [QueryField.filter]: filter, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: 1, - number: 1, - }, - }; - - return defaultQuery; -} - -describe('QueryCheckSelectField', () => { - let queryCheckSelectField: QueryCheckSelectField; - let configParam: ConfigParam; - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: CONTROL_OPTIONS_TOKEN, - useValue: { - requiredSelectField: false, - debug: false, - }, - }, - QueryCheckSelectField, - ], - }).compile(); - - queryCheckSelectField = module.get>( - QueryCheckSelectField - ); - configParam = module.get(CONTROL_OPTIONS_TOKEN); - }); - - it('Is valid', () => { - const query = getDefaultQuery(); - expect(queryCheckSelectField.transform(query)).toEqual(query); - }); - - it('Is invalid', () => { - const query = getDefaultQuery(); - jest.mocked(configParam).requiredSelectField = true; - expect.assertions(1); - try { - queryCheckSelectField.transform(query); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts deleted file mode 100644 index 9e8579c2..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-check-select-field/query-check-select-field.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BadRequestException, Inject, PipeTransform } from '@nestjs/common'; -import { CONTROL_OPTIONS_TOKEN } from '../../../constants'; -import { ConfigParam, Entity, ValidateQueryError } from '../../../types'; -import { Query } from '../../../helper'; - -export class QueryCheckSelectField - implements PipeTransform, Query> -{ - @Inject(CONTROL_OPTIONS_TOKEN) private configParam!: ConfigParam; - transform(value: Query): Query { - if (this.configParam.requiredSelectField && value.fields === null) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Fields params in query is required'`, - path: ['fields'], - }; - throw new BadRequestException([error]); - } - return value; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts deleted file mode 100644 index 82b1b95f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './query-filed-in-include.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts deleted file mode 100644 index c056f542..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { BadRequestException } from '@nestjs/common'; -import { QueryFiledInIncludePipe } from './query-filed-in-include.pipe'; -import { Users } from '../../../mock-utils'; -import { Query, QueryField } from '../../../helper'; - -describe('QueryFiledInIncludePipe', () => { - let queryFiledInIncludePipe: QueryFiledInIncludePipe; - - beforeAll(() => { - queryFiledInIncludePipe = new QueryFiledInIncludePipe(); - }); - - it('Should be ok', () => { - const check: Query = { - [QueryField.fields]: { - roles: ['id'], - }, - [QueryField.include]: ['roles'], - [QueryField.filter]: { - target: null, - relation: null, - }, - [QueryField.sort]: null, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - - const check2: Query = { - [QueryField.fields]: null, - [QueryField.include]: ['roles'], - [QueryField.filter]: { - target: null, - relation: { - roles: { name: { eq: 'test' } }, - }, - }, - [QueryField.sort]: null, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - - const result = queryFiledInIncludePipe.transform(check); - expect(result).toEqual(check); - const result2 = queryFiledInIncludePipe.transform(check2); - expect(result2).toEqual(check2); - }); - - it('Should be not ok', () => { - const check: Query = { - [QueryField.fields]: { - roles: ['id'], - }, - [QueryField.include]: null, - [QueryField.filter]: { - target: null, - relation: null, - }, - [QueryField.sort]: null, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - const check2: Query = { - [QueryField.fields]: { - roles: ['id'], - }, - [QueryField.include]: null, - [QueryField.filter]: { - target: null, - relation: null, - }, - [QueryField.sort]: { - addresses: { - id: 'ASC', - }, - }, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - - const check3: Query = { - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.filter]: { - target: null, - relation: { - roles: { name: { eq: 'test' } }, - }, - }, - [QueryField.sort]: null, - [QueryField.page]: { - number: 1, - size: 1, - }, - }; - expect.assertions(3); - try { - queryFiledInIncludePipe.transform(check); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - try { - queryFiledInIncludePipe.transform(check2); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - try { - queryFiledInIncludePipe.transform(check3); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts deleted file mode 100644 index 076cf0c7..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { BadRequestException, PipeTransform } from '@nestjs/common'; -import { Entity, ValidateQueryError } from '../../../types'; -import { ObjectTyped, Query } from '../../../helper'; -export class QueryFiledInIncludePipe - implements PipeTransform, Query> -{ - transform(value: Query): Query { - const errors: ValidateQueryError[] = []; - - const { fields, include, sort, filter } = value; - const includeSet = new Set(); - - if (include) { - include.reduce((acum, item) => acum.add(item), includeSet); - } - - if (filter) { - const { relation } = filter; - if (relation) { - const filterRelationFields = ObjectTyped.keys(relation); - const filterFieldsErrors = filterRelationFields - .filter((i) => !includeSet.has(i.toString())) - .map((i) => ({ - code: 'invalid_intersection_types', - message: `Add '${i.toString()}' to query param 'include'`, - path: ['filter', 'relation', i.toString()], - })); - - errors.push(...filterFieldsErrors); - } - } - - if (fields) { - const { target: targetResourceFields, ...relationFields } = fields; - const selectRelationFields = ObjectTyped.keys(relationFields); - const fieldsErrors = selectRelationFields - .filter((i) => !includeSet.has(i.toString())) - .map((i) => ({ - code: 'invalid_intersection_types', - message: `Add '${i.toString()}' to query param 'include'`, - path: ['fields'], - })); - - errors.push(...fieldsErrors); - } - - if (sort) { - const { target: targetResourceSorts, ...relationSorts } = sort; - const selectRelationFields = ObjectTyped.keys(relationSorts); - const fieldsErrors = selectRelationFields - .filter((i) => !includeSet.has(i.toString())) - .map((i) => ({ - code: 'invalid_intersection_types', - message: `Add '${i.toString()}' to query param 'include'`, - path: ['sort'], - })); - - errors.push(...fieldsErrors); - } - - if (errors.length > 0) { - throw new BadRequestException(errors); - } - - return value; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts deleted file mode 100644 index 7422bf5a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './query-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts deleted file mode 100644 index 115d88ee..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; -import { IMemoryDb } from 'pg-mem'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { - CurrentDataSourceProvider, - ZodInputQuerySchema, -} from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - DEFAULT_PAGE_SIZE, - DEFAULT_QUERY_PAGE, - ZOD_INPUT_QUERY_SCHEMA, -} from '../../../constants'; -import { QueryInputPipe } from './query-input.pipe'; -import { - QueryField, - ZodInputQuerySchema as TypeZodInputQuerySchema, -} from '../../../helper'; - -const page = { - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, -}; - -describe('QueryInputPipe', () => { - let db: IMemoryDb; - let queryInputPipe: QueryInputPipe; - let zodParse: TypeZodInputQuerySchema; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - ZodInputQuerySchema(Users), - QueryInputPipe, - ], - }).compile(); - - queryInputPipe = module.get>(QueryInputPipe); - zodParse = module.get>( - ZOD_INPUT_QUERY_SCHEMA - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', () => { - const input = {}; - const result = queryInputPipe.transform(input); - - const input1 = { - [QueryField.page]: page[QueryField.page], - }; - const result1 = queryInputPipe.transform(input1); - - const input2 = { - ...page, - [QueryField.fields]: { target: 'test', addresses: '' }, - }; - const result2 = queryInputPipe.transform(input2); - - expect(result).toEqual(page); - expect(result1).toEqual(input1); - expect(result2).toEqual(input2); - }); - - it('Should be not ok', () => { - const input = { - [QueryField.page]: { - size: 0, - number: 'sdfsdf', - }, - sdaas: 'sdfsdf', - [QueryField.include]: null, - [QueryField.fields]: { - dsada: 'sdfsf', - sdfsdfsdfs: 'sdfsdf', - }, - [QueryField.sort]: null, - [QueryField.filter]: { - id: { - eq: 'null', - }, - }, - }; - expect.assertions(1); - try { - const result = queryInputPipe.transform(input); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodParse, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - const result = queryInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts deleted file mode 100644 index 9d740098..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query-input/query-input.pipe.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - Inject, - PipeTransform, - BadRequestException, - InternalServerErrorException, -} from '@nestjs/common'; -import { ZodError } from 'zod'; -import { errorMap } from 'zod-validation-error'; -import { ZOD_INPUT_QUERY_SCHEMA } from '../../../constants'; -import { ZodInputQuerySchema, InputQuery } from '../../../helper'; -import { Entity, JSONValue } from '../../../types'; - -export class QueryInputPipe - implements PipeTransform> -{ - @Inject(ZOD_INPUT_QUERY_SCHEMA) - private zodInputQuerySchema!: ZodInputQuerySchema; - - transform(value: JSONValue): InputQuery { - try { - return this.zodInputQuerySchema.parse(value, { - errorMap: errorMap, - }); - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts deleted file mode 100644 index a8853ae0..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './query.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts deleted file mode 100644 index e874744a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; -import { QueryPipe } from '../query'; -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; -import { - QueryField, - ZodQuerySchema as TypeZodQuerySchema, -} from '../../../helper'; - -import { CurrentDataSourceProvider, ZodQuerySchema } from '../../../factory'; -import { - DEFAULT_CONNECTION_NAME, - ZOD_QUERY_SCHEMA, - DEFAULT_PAGE_SIZE, - DEFAULT_QUERY_PAGE, -} from '../../../constants'; -import { TransformInputService } from '../../../service'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; - -describe('QueryPipe', () => { - let db: IMemoryDb; - let queryPipe: QueryPipe; - let zodParse: TypeZodQuerySchema; - let transformInputService: TransformInputService; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - ZodQuerySchema(Users), - TransformInputService, - QueryPipe, - ], - }).compile(); - - queryPipe = module.get>(QueryPipe); - zodParse = module.get>(ZOD_QUERY_SCHEMA); - transformInputService = module.get>( - TransformInputService - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Should be ok', () => { - const filter = { - target: null, - relation: null, - }; - - const result = queryPipe.transform({ - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }); - expect(result).toEqual({ - [QueryField.filter]: filter, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }); - }); - - it('Should be not ok', () => { - const input = { - filter: { - test: '', - }, - [QueryField.page]: { - size: DEFAULT_PAGE_SIZE, - number: DEFAULT_QUERY_PAGE, - }, - }; - expect.assertions(1); - try { - const result = queryPipe.transform(input as any); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest - .spyOn(transformInputService, 'transformSort') - .mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - const result = queryPipe.transform({} as any); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts deleted file mode 100644 index 1ea63146..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/pipe/query/query.pipe.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { ZodError } from 'zod'; - -import { InputQuery, Query, QueryField, ZodQuerySchema } from '../../../helper'; -import { Entity, JSONValue } from '../../../types'; -import { ZOD_QUERY_SCHEMA } from '../../../constants'; -import { TransformInputService } from '../../../service'; - -export class QueryPipe - implements PipeTransform, Query> -{ - @Inject(ZOD_QUERY_SCHEMA) - private zodQuerySchema!: ZodQuerySchema; - - @Inject(TransformInputService) - private transformInputService!: TransformInputService; - - transform(value: InputQuery): Query { - try { - const { filter, page, sort, include, fields } = value; - const queryObject: JSONValue = { - [QueryField.filter]: this.transformInputService.transformFilter(filter), - [QueryField.fields]: this.transformInputService.transformFields(fields), - [QueryField.include]: - this.transformInputService.transformInclude(include), - [QueryField.sort]: this.transformInputService.transformSort(sort), - [QueryField.page]: page, - }; - return this.zodQuerySchema.parse(queryObject); - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts deleted file mode 100644 index 442fee03..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './typeorm-utils.service'; -export * from './transform-data.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts deleted file mode 100644 index 6ffaf430..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/swagger-bind.service.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import { DiscoveryService } from '@nestjs/core'; -import { PARAMTYPES_METADATA } from '@nestjs/common/constants'; -import { ApiExtraModels, ApiTags } from '@nestjs/swagger'; -import { DECORATORS } from '@nestjs/swagger/dist/constants'; -import { JsonBaseController } from '../controller/json-base.controller'; -import { Repository } from 'typeorm'; -import { - CONTROL_OPTIONS_TOKEN, - CURRENT_ENTITY_REPOSITORY, - JSON_API_CONTROLLER_POSTFIX, - JSON_API_DECORATOR_ENTITY, - SWAGGER_METHOD, -} from '../../constants'; - -import { - createApiModels, - ObjectTyped, - swaggerMethod, - SwaggerMethod, - FilterOperand, - nameIt, - getProviderName, -} from '../../helper'; -import { Bindings } from '../../config/bindings'; -import { DecoratorOptions, Entity } from '../../types'; - -@Injectable() -export class SwaggerBindService implements OnModuleInit { - @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; - @Inject(DiscoveryService) private discoveryService!: DiscoveryService; - @Inject(SWAGGER_METHOD) private swaggerMethod!: SwaggerMethod; - @Inject(CONTROL_OPTIONS_TOKEN) private config!: DecoratorOptions; - - public initSwagger() { - const controllerName = nameIt( - getProviderName(this.repository.target, JSON_API_CONTROLLER_POSTFIX), - JsonBaseController - ).name; - - const controllerInst = this.discoveryService - .getControllers() - .find( - (i) => - i.name === controllerName || - this.repository.target === - Reflect.getMetadata( - JSON_API_DECORATOR_ENTITY, - i.instance.constructor - ) - ); - if (!controllerInst) - throw new Error( - `Controller for ${this.repository.metadata.name} is empty` - ); - const controller = controllerInst.instance.constructor; - const apiTag = Reflect.getMetadata(DECORATORS.API_TAGS, controller); - if (!apiTag) { - ApiTags(this.config['overrideRoute'] || this.repository.metadata.name)( - controller - ); - } - - ApiTags(this.repository.metadata.name)(controller); - - ApiExtraModels(FilterOperand)(controller); - ApiExtraModels(createApiModels(this.repository))(controller); - - const { allowMethod = Object.keys(Bindings) } = this.config; - for (const method of ObjectTyped.keys(Bindings)) { - if (!allowMethod.includes(method)) continue; - - if (method in swaggerMethod) { - swaggerMethod[method]( - controller.prototype, - this.repository, - Bindings[method], - this.config - ); - } - Reflect.defineMetadata( - PARAMTYPES_METADATA, - [Object], - controller.prototype, - method - ); - } - } - - onModuleInit(): any { - this.initSwagger(); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts deleted file mode 100644 index 777d29a5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.spec.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - Addresses, - Comments, - createAndPullSchemaBase, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../constants'; -import { TransformDataService } from './transform-data.service'; -import { TypeormUtilsService } from './typeorm-utils.service'; -import { ApplicationConfig } from '@nestjs/core'; -import { VersioningType } from '@nestjs/common'; -import { EntityPropsMapService } from '../../service'; -import { data } from '../../helper/zod/zod-input-post-relationship-schema'; - -describe('TransformDataService', () => { - let db: IMemoryDb; - let transformDataService: TransformDataService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - let applicationConfig: ApplicationConfig; - let entityPropsMapService: EntityPropsMapService; - let usersData: Users; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - TransformDataService, - EntityPropsMapService, - ], - }).compile(); - - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - - // transformDataService = - // module.get>(TransformDataService); - entityPropsMapService = module.get( - EntityPropsMapService - ); - - const data = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - userGroup: true, - manager: true, - roles: true, - addresses: true, - comments: true, - notes: true, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - usersData = data; - applicationConfig = module.get(ApplicationConfig); - }); - - beforeEach(() => { - transformDataService = new TransformDataService(); - Object.defineProperty(transformDataService, 'applicationConfig', { - value: applicationConfig, - }); - Object.defineProperty(transformDataService, 'entityPropsMapService', { - value: entityPropsMapService, - }); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Url path', () => { - const prefix = 'api'; - applicationConfig.setGlobalPrefix(prefix); - - expect(transformDataService.urlPath).toEqual(['', prefix]); - }); - it('Url path with version', () => { - const prefix = 'api'; - const version = '1'; - - applicationConfig.setGlobalPrefix(prefix); - - jest - .spyOn(applicationConfig, 'getVersioning') - .mockImplementationOnce(() => ({ - type: VersioningType.URI, - defaultVersion: version, - })); - - expect(transformDataService.urlPath).toEqual(['', prefix, 'v1']); - }); - - it('check type, id and links', () => { - const result = transformDataService.transformData(usersData); - expect(result).toHaveProperty('data'); - const { data } = result; - expect(data.type).toBe('users'); - expect(data.id).toBe(usersData.id.toString()); - expect(data.links.self).toBe( - [...transformDataService.urlPath, 'users', usersData.id.toString()].join( - '/' - ) - ); - }); - - it('check attributes', async () => { - const result = transformDataService.transformData(usersData); - const { - id, - addresses, - comments, - manager, - roles, - notes, - userGroup, - ...other - } = usersData; - expect(result.data.attributes).toEqual(other); - const result1 = transformDataService.transformData([]); - expect(result1.data).toEqual([]); - - const data = await userRepository.findOne({ - select: { - id: true, - login: true, - isActive: true, - testDate: true, - }, - where: { - id: 1, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - const result2 = transformDataService.transformData(data); - const { id: id2, ...other2 } = data; - - expect(result2.data.attributes).toEqual(other2); - }); - - it('check relationships', async () => { - const { - data: { relationships }, - } = transformDataService.transformData(usersData); - expect(relationships?.addresses?.data).toEqual({ - type: 'addresses', - id: usersData.addresses.id.toString(), - }); - expect(relationships?.addresses?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/addresses` - ); - expect(relationships?.manager?.data).toEqual({ - type: 'users', - id: usersData.manager.id.toString(), - }); - expect(relationships?.manager?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/manager` - ); - expect(relationships?.roles?.data).toEqual( - usersData.roles.map((i) => ({ - type: 'roles', - id: i.id.toString(), - })) - ); - expect(relationships?.roles?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/roles` - ); - expect(relationships?.comments?.data).toEqual( - usersData.comments.map((i) => ({ - type: 'comments', - id: i.id.toString(), - })) - ); - expect(relationships?.comments?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/comments` - ); - expect(relationships?.notes?.data).toEqual( - usersData.notes.map((i) => ({ - type: 'notes', - id: i.id.toString(), - })) - ); - expect(relationships?.notes?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/notes` - ); - expect(relationships?.userGroup?.data).toEqual({ - type: 'user-groups', - id: usersData.userGroup.id.toString(), - }); - expect(relationships?.userGroup?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/userGroup` - ); - }); - - it('check relationships again', async () => { - const data = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - userGroup: true, - roles: true, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - - const { - data: { relationships }, - } = transformDataService.transformData(data); - - expect(relationships?.addresses).not.toHaveProperty('data'); - expect(relationships?.addresses?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/addresses` - ); - expect(relationships?.manager).not.toHaveProperty('data'); - expect(relationships?.manager?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/manager` - ); - expect(relationships?.roles?.data).toEqual( - usersData.roles.map((i) => ({ - type: 'roles', - id: i.id.toString(), - })) - ); - expect(relationships?.roles?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/roles` - ); - expect(relationships?.comments).not.toHaveProperty('data'); - expect(relationships?.comments?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/comments` - ); - expect(relationships?.notes).not.toHaveProperty('data'); - expect(relationships?.notes?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/notes` - ); - expect(relationships?.userGroup?.data).toEqual({ - type: 'user-groups', - id: usersData.userGroup.id.toString(), - }); - expect(relationships?.userGroup?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/userGroup` - ); - data.userGroup = null as any; - data.roles = []; - const { - data: { relationships: relationships2 }, - } = transformDataService.transformData(data); - - expect(relationships2?.addresses).not.toHaveProperty('data'); - expect(relationships2?.addresses?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/addresses` - ); - expect(relationships2?.manager).not.toHaveProperty('data'); - expect(relationships2?.manager?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/manager` - ); - - expect(relationships2?.roles?.data).toEqual([]); - expect(relationships2?.roles?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/roles` - ); - expect(relationships2?.comments).not.toHaveProperty('data'); - expect(relationships2?.comments?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/comments` - ); - expect(relationships2?.notes).not.toHaveProperty('data'); - expect(relationships2?.notes?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/notes` - ); - expect(relationships2?.userGroup?.data).toEqual(null); - expect(relationships2?.userGroup?.links.self).toBe( - `${transformDataService.urlPath.join('/')}/users/${ - usersData.id - }/relationships/userGroup` - ); - }); - - it('check include', async () => { - const data = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - userGroup: true, - roles: true, - }, - }); - if (!data) { - throw new Error('Not found user'); - } - - const { included } = transformDataService.transformData(data); - expect(included).not.toBe(undefined); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts deleted file mode 100644 index 7fac3b28..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/transform-data.service.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { Inject, Injectable, VersioningType } from '@nestjs/common'; -import { ApplicationConfig } from '@nestjs/core'; - -import { - Attributes, - Data, - Entity, - EntityRelation, - MainData, - Relationships, - ResourceData, -} from '../../types'; -import { ResourceObject } from '../../types/response'; -import { camelToKebab, ObjectTyped } from '../../helper'; -import { RoutePathFactory } from '@nestjs/core/router/route-path-factory'; -import { EntityPropsMapService } from '../../service'; - -Injectable(); -export class TransformDataService { - @Inject(ApplicationConfig) private applicationConfig!: ApplicationConfig; - @Inject(EntityPropsMapService) - private entityPropsMapService!: EntityPropsMapService; - - private _urlPath!: string[]; - - get urlPath() { - if (this._urlPath) return [...this._urlPath]; - this._urlPath = ['']; - const prefix = this.applicationConfig.getGlobalPrefix(); - const version = this.applicationConfig.getVersioning(); - - const routePathFactory = new RoutePathFactory(this.applicationConfig); - - if (prefix) { - this._urlPath.push(this.applicationConfig.getGlobalPrefix()); - } - if (version && version.type === VersioningType.URI) { - const firstVersion = Array.isArray(version.defaultVersion) - ? version.defaultVersion[0] - : version.defaultVersion; - if (firstVersion) { - this._urlPath.push( - `${routePathFactory.getVersionPrefix( - version - )}${firstVersion.toString()}` - ); - } - } - return [...this._urlPath]; - } - private getLink(...partOfUrl: string[]) { - const urlPath = this.urlPath; - urlPath.push(...partOfUrl); - return urlPath.join('/'); - } - - public transformData(data: E): Pick, 'data' | 'included'>; - public transformData( - data: E[] - ): Pick, 'data' | 'included'>; - public transformData( - data: E | E[] - ): - | Pick, 'data' | 'included'> - | Pick, 'data' | 'included'> { - const dataResponse = Array.isArray(data) - ? data - .map((i) => this.getMainData(i)) - .filter((i: ResourceData | null): i is ResourceData => !!i) - : this.getMainData(data); - - let relationships: Relationships | undefined = undefined; - if (Array.isArray(dataResponse) && dataResponse.length > 0) { - relationships = dataResponse.reduce( - (acum, item) => ({ - ...acum, - ...item.relationships, - }), - {} as Relationships - ); - } - - if (!Array.isArray(dataResponse) && dataResponse !== null) { - relationships = dataResponse.relationships; - } - - if (relationships === undefined) { - return { data: dataResponse } as Pick< - ResourceObject, - 'data' | 'included' - >; - } - - const propsNeedForInclude = ObjectTyped.entries(relationships).reduce( - (acum, [key, val]) => { - if (!val || !('data' in val)) { - return acum; - } - if (Array.isArray(val.data)) { - acum.push(key); - return acum; - } - if (!Array.isArray(val.data)) { - acum.push(key); - } - return acum; - }, - [] as EntityRelation[] - ); - - const included = (Array.isArray(data) ? data : [data]) - .reduce((acum, item) => { - const tmp = propsNeedForInclude.reduce((a, props) => { - const currentItem = item[props]; - type CurrentItem = typeof currentItem; - const r = ( - Array.isArray(currentItem) ? currentItem : [currentItem] - ) as CurrentItem[]; - - a.push(...r); - return a; - }, [] as E[EntityRelation][]); - acum.push(...tmp); - return acum; - }, [] as E[EntityRelation][]) - .map((i) => { - return this.getMainData(i as E); - }) - .filter((i) => !!i); - - if (included.length > 0) { - return { data: dataResponse, included } as unknown as Pick< - ResourceObject, - 'data' | 'included' - >; - } - return { data: dataResponse } as unknown as Pick< - ResourceObject, - 'data' | 'included' - >; - } - - getMainData(data: E): ResourceData | null { - if (!data) return null; - const entity = data.constructor as E; - return { - ...this.getData( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[this.entityPropsMapService.getPrimaryColumnsForEntity(entity)] as - | string - | number - ), - attributes: ObjectTyped.entries(data).reduce((acum, [key, val]) => { - if ( - this.entityPropsMapService - .getRelPropsForEntity(entity) - .includes(key.toString()) || - key.toString() === - this.entityPropsMapService.getPrimaryColumnsForEntity(entity) - ) { - return acum; - } - acum[key] = val; - return acum; - }, {} as Record) as Attributes, - relationships: this.transformRelationshipsData(data), - links: { - self: this.getLink( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[ - this.entityPropsMapService.getPrimaryColumnsForEntity(entity) - ] as string - ), - }, - }; - } - - getRelationships>( - data: E, - rel: Rel - ): Data { - const entity = data.constructor as E; - const relation = data[rel]; - const target = this.entityPropsMapService.getRelationPropsType(entity, rel); - const typeName = camelToKebab( - this.entityPropsMapService.getNameForEntity(target) - ) as Rel; - - const primaryColumn = - this.entityPropsMapService.getPrimaryColumnsForEntity(target); - - const resultData = Array.isArray(relation) - ? (relation as E[Rel][]).map((i: E[Rel]) => - this.getData( - typeName, - i[primaryColumn as keyof E[Rel]] as string - ) - ) - : relation === null || !relation - ? null - : this.getData( - typeName, - relation[primaryColumn as keyof E[Rel]] as string - ); - - return { - data: resultData, - } as Data; - } - - transformRelationshipsData(data: E): Relationships { - const entity = data.constructor as E; - return this.entityPropsMapService - .getRelPropsForEntity(entity) - .reduce((acum, val) => { - const result: { - links: Record; - data?: Data], EntityRelation>['data']; - } = { - links: { - self: this.getLink( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[ - this.entityPropsMapService.getPrimaryColumnsForEntity( - entity - ) as keyof E - ] as string, - 'relationships', - val - ), - }, - }; - - if (val in data) { - const { data: dataRelationships } = this.getRelationships( - data, - val as EntityRelation - ); - if (Array.isArray(dataRelationships)) { - result['data'] = dataRelationships; - } - - if (!Array.isArray(dataRelationships)) { - result['data'] = dataRelationships; - } - } - acum[val.toString()] = result; - return acum; - }, {} as Record) as Relationships; - } - - getData(type: T, id: number | string): MainData { - return { - type, - id: id.toString(), - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts deleted file mode 100644 index 3682c037..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.spec.ts +++ /dev/null @@ -1,768 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - UserGroups, - Users, - Comments, - Roles, - Addresses, - Notes, - getRepository, - pullAllData, -} from '../../mock-utils'; -import { - CurrentDataSourceProvider, - EntityRepositoryFactory, -} from '../../factory'; -import { - CURRENT_ENTITY_REPOSITORY, - DEFAULT_CONNECTION_NAME, -} from '../../constants'; -import { TypeormUtilsService } from './typeorm-utils.service'; -import { - PostData, - PostRelationshipData, - Query, - QueryField, -} from '../../helper'; -import { - EXPRESSION, - FilterOperand, - OperandsMapExpression, - Entity, -} from '../../types'; -import { - BadRequestException, - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; - -function getDefaultQuery() { - const filter = { - relation: null, - target: null, - }; - const defaultQuery: Query = { - [QueryField.filter]: filter, - [QueryField.fields]: null, - [QueryField.include]: null, - [QueryField.sort]: null, - [QueryField.page]: { - size: 1, - number: 1, - }, - }; - - return defaultQuery; -} - -describe('TypeormUtilsService', () => { - let db: IMemoryDb; - let typeormUtilsServiceUserGroups: TypeormUtilsService; - let repositoryUserGroups: Repository; - - let typeormUtilsServiceUser: TypeormUtilsService; - let repositoryUser: Repository; - - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - - function getQuery() { - return repositoryUser - .createQueryBuilder() - .subQuery() - .select('Users-Roles.user_id') - .from('users_have_roles', 'Users-Roles') - .leftJoin( - Roles, - 'Users__Roles_roles', - 'Users-Roles.role_id = Users__Roles_roles.id' - ); - } - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(UserGroups), - TypeormUtilsService, - ], - }).compile(); - - typeormUtilsServiceUserGroups = - module.get>(TypeormUtilsService); - repositoryUserGroups = module.get>( - CURRENT_ENTITY_REPOSITORY - ); - - const moduleUsers: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityRepositoryFactory(Users), - TypeormUtilsService, - ], - }).compile(); - - ({ - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - } = getRepository(module)); - await pullAllData( - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository - ); - - typeormUtilsServiceUser = - moduleUsers.get>(TypeormUtilsService); - repositoryUser = moduleUsers.get>( - CURRENT_ENTITY_REPOSITORY - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('TypeormUtilsService.currentAlias', () => { - expect(typeormUtilsServiceUserGroups.currentAlias).toBe('UserGroups'); - }); - - it('TypeormUtilsService.getAliasForRelation', () => { - expect(typeormUtilsServiceUserGroups.getAliasForRelation('users')).toBe( - 'UserGroups__Users_users' - ); - }); - - it('TypeormUtilsService.getAliasPath', () => { - expect(typeormUtilsServiceUserGroups.getAliasPath('id')).toBe( - 'UserGroups.id' - ); - expect( - typeormUtilsServiceUserGroups.getAliasPath('Users', 'UserGroups') - ).toBe('UserGroups.Users'); - expect( - typeormUtilsServiceUserGroups.getAliasPath('Users', 'UserGroups', '-') - ).toBe('UserGroups-Users'); - expect( - typeormUtilsServiceUserGroups.getAliasPath('label', 'users', '-') - ).toBe('Users-label'); - }); - - describe('asyncIterateFindRelationships', () => { - it('should be ok', async () => { - const notes = await notesRepository.find(); - const userGroup = await userGroupRepository.find(); - - const data: PostData['relationships'] = { - notes: [ - { - type: 'notes', - id: notes[0].id, - }, - ], - manager: { - type: 'users', - id: '1', - }, - userGroup: { - type: 'users-group', - id: `${userGroup[0].id}`, - }, - }; - - const result = []; - for await (const item of typeormUtilsServiceUser.asyncIterateFindRelationships( - data - )) { - result.push(item); - } - - expect(result[0]).toHaveProperty('notes'); - expect(result[0]['notes']).toEqual([{ id: notes[0].id }]); - - expect(result[1]).toHaveProperty('manager'); - expect(result[1]['manager']).toEqual({ id: 1 }); - - expect(result[2]).toHaveProperty('userGroup'); - expect(result[2]['userGroup']).toEqual({ id: userGroup[0].id }); - }); - - it('should be error props incorrect', async () => { - const data = { - incorrectProps: { - type: 'users', - id: '1', - }, - } as any; - expect.assertions(1); - try { - await typeormUtilsServiceUser - .asyncIterateFindRelationships(data) - .next(); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('should be error resource not found', async () => { - const data: PostData['relationships'] = { - manager: { - id: '1000', - type: 'users', - }, - }; - expect.assertions(1); - try { - await typeormUtilsServiceUser - .asyncIterateFindRelationships(data) - .next(); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - }); - - describe('getFilterExpressionForTarget', () => { - it('expression for target field with null', () => { - - const nullableField = 'id' - const notNullableField = 'login' - const query = getDefaultQuery(); - query.filter.target = { - [nullableField]: { - [FilterOperand.eq]: null, - }, - [notNullableField]: { - [FilterOperand.ne]: null, - }, - }; - - function guardField( - filter: any, - field: any - ): asserts field is keyof R { - if (filter && !(field in filter)) - throw new Error('field not exist in query filter'); - } - - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - const mainAliasCheck = 'Users'; - - - for (const item of result) { - const { params, alias, expression, selectInclude } = item; - expect(selectInclude).toBe(undefined); - if (!alias) { - expect(alias).not.toBe(undefined); - throw new Error('alias in undefined for result'); - } - const [mainAlias, field] = alias.split('.'); - expect(mainAlias).toBe(mainAliasCheck); - guardField(query.filter.target, field); - const filterName: any = query.filter.target[field]; - if (!filterName) { - expect(filterName).not.toBe(undefined); - throw new Error('filterName in undefined from query'); - } - - expect(params).toBe(undefined) - - if (field === nullableField) { - expect(expression).toBe('IS NULL'); - continue; - } - - if (field === notNullableField) { - expect(expression).toBe('IS NOT NULL'); - continue; - } - - throw new Error('filed is incorrect'); - } - }) - it('expression for target field', () => { - const valueTest = (filterOperand: FilterOperand) => - `test for ${filterOperand}`; - const valueTestArray = ( - filterOperand: FilterOperand.nin | FilterOperand.in - ): [string, ...string[]] => [valueTest(filterOperand)]; - - const query = getDefaultQuery(); - query.filter.target = { - id: { - [FilterOperand.eq]: valueTest(FilterOperand.eq), - [FilterOperand.ne]: valueTest(FilterOperand.ne), - }, - isActive: { - [FilterOperand.like]: valueTest(FilterOperand.like), - [FilterOperand.regexp]: valueTest(FilterOperand.regexp), - }, - firstName: { - [FilterOperand.gt]: valueTest(FilterOperand.gt), - [FilterOperand.gte]: valueTest(FilterOperand.gte), - }, - testDate: { - [FilterOperand.lt]: valueTest(FilterOperand.lt), - [FilterOperand.lte]: valueTest(FilterOperand.lt), - }, - createdAt: { - [FilterOperand.in]: valueTestArray(FilterOperand.in), - [FilterOperand.nin]: valueTestArray(FilterOperand.nin), - }, - }; - - function guardField( - filter: any, - field: any - ): asserts field is keyof R { - if (filter && !(field in filter)) - throw new Error('field not exist in query filter'); - } - - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - const mainAliasCheck = 'Users'; - const paramsNameSet = new Set(); - for (const item of result) { - const { params, alias, expression, selectInclude } = item; - expect(selectInclude).toBe(undefined); - if (!alias) { - expect(alias).not.toBe(undefined); - throw new Error('alias in undefined for result'); - } - const [mainAlias, field] = alias.split('.'); - expect(mainAlias).toBe(mainAliasCheck); - guardField(query.filter.target, field); - const filterName: any = query.filter.target[field]; - if (!filterName) { - expect(filterName).not.toBe(undefined); - throw new Error('filterName in undefined from query'); - } - if (!params) { - expect(params).not.toBe(undefined); - throw new Error('params in undefined for result'); - } - if (Array.isArray(params)) { - expect(params).not.toBeInstanceOf(Array); - throw new Error('params in undefined for result'); - } - const { val, name } = params; - expect(paramsNameSet.has(name)).toBe(false); - paramsNameSet.add(name); - const reg = new RegExp(`params_${alias}_\\d{1,}`); - const regResult = name.match(reg); - - if (regResult === null) { - expect(name.match(reg)).not.toBe(null); - throw new Error(`name is not pattern: params_${alias}_\\d{1,}`); - } - const expressionMap = expression.replace(name, EXPRESSION); - const checkFilterOperand = Object.entries(FilterOperand).find( - ([key, val]) => OperandsMapExpression[val] === expressionMap - ); - if (!checkFilterOperand) { - expect(checkFilterOperand).not.toBe(undefined); - throw new Error(`expression incorrect`); - } - - const operand = checkFilterOperand[0] as any; - guardField(filterName, operand); - if (operand === 'like') { - expect(params.val).toEqual(`%${filterName[operand]}%`); - } else { - expect(params.val).toEqual(filterName[operand]); - } - } - }); - it('expression for target relation field with relation column', () => { - const query = getDefaultQuery(); - query.filter.target = { - addresses: { - [FilterOperand.eq]: 'null', - [FilterOperand.ne]: 'null', - }, - }; - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - expect(result.length).toBe(2); - const [first, second] = result; - expect(first).not.toHaveProperty('params'); - expect(first).not.toHaveProperty('selectInclude'); - expect(first['alias']).toBe('Users.addresses'); - expect(first['expression']).toBe('IS NULL'); - expect(second).not.toHaveProperty('params'); - expect(second).not.toHaveProperty('selectInclude'); - expect(second['alias']).toBe('Users.addresses'); - expect(second['expression']).toBe('IS NOT NULL'); - }); - it('expression for target relation field with one-to-many', () => { - const query = getDefaultQuery(); - query.filter.target = { - comments: { - [FilterOperand.eq]: 'null', - [FilterOperand.ne]: 'null', - }, - }; - const subQuery = repositoryUser - .createQueryBuilder() - .subQuery() - .select('Comments.createdBy', 'createdBy') - .from(Comments, 'Comments') - .where(`Comments.createdBy = Users.id`) - .getQuery(); - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - expect(result.length).toBe(2); - const [first, second] = result; - expect(first).not.toHaveProperty('params'); - expect(first).not.toHaveProperty('selectInclude'); - expect(first).not.toHaveProperty('alias'); - expect(first['expression']).toBe(`NOT EXISTS ${subQuery}`); - expect(second).not.toHaveProperty('params'); - expect(second).not.toHaveProperty('selectInclude'); - expect(second).not.toHaveProperty('alias'); - expect(second['expression']).toBe(`EXISTS ${subQuery}`); - }); - it('expression for target relation field with many-to-many', () => { - const query = getDefaultQuery(); - query.filter.target = { - roles: { - [FilterOperand.eq]: 'null', - [FilterOperand.ne]: 'null', - }, - }; - const subQuery = getQuery() - .where(`Users-Roles.user_id = Users.id`) - .getQuery(); - const result = - typeormUtilsServiceUser.getFilterExpressionForTarget(query); - - expect(result.length).toBe(2); - const [first, second] = result; - expect(first).not.toHaveProperty('params'); - expect(first).not.toHaveProperty('selectInclude'); - expect(first).not.toHaveProperty('alias'); - expect(first['expression']).toBe(`NOT EXISTS ${subQuery}`); - expect(second).not.toHaveProperty('params'); - expect(second).not.toHaveProperty('selectInclude'); - expect(second).not.toHaveProperty('alias'); - expect(second['expression']).toBe(`EXISTS ${subQuery}`); - }); - }); - - describe('getFilterExpressionForRelation', () => { - it('expression for relation many-to-many', () => { - const query = getDefaultQuery(); - const conditional = { - name: { - [FilterOperand.eq]: 'null', - [FilterOperand.ne]: 'null', - }, - createdAt: { - [FilterOperand.eq]: 'test1', - [FilterOperand.ne]: 'test2', - [FilterOperand.nin]: ['test3'] as [string, ...string[]], - }, - }; - - query.filter.relation = { - roles: conditional, - }; - - let subQuery = getQuery() - .where(`"Users__Roles_roles"."name" IS NULL`) - .andWhere(`"Users__Roles_roles"."name" IS NOT NULL`) - .andWhere(`"Users__Roles_roles"."created_at" = :param1`) - .andWhere(`"Users__Roles_roles"."created_at" <> :param2`) - .andWhere(`"Users__Roles_roles"."created_at" NOT IN (:...param3)`) - .getQuery(); - - const result = - typeormUtilsServiceUser.getFilterExpressionForRelation(query); - - expect(result.length).toBe(1); - - const [first] = result; - expect(first).not.toHaveProperty('selectInclude'); - if (!first.params && !Array.isArray(first.params)) { - expect(first).toHaveProperty('params'); - expect(first.params).toBeInstanceOf(Array); - } - if (Array.isArray(first.params)) { - expect(first?.params?.length).toBe(3); - const [firstParams, secondParams, thirdParams] = first.params; - expect(firstParams?.val).toBe( - query.filter?.relation?.roles?.createdAt?.eq - ); - - const regResult1 = firstParams?.name.match( - new RegExp(`params_Roles.createdAt_\\d{1,}`) - ); - if (regResult1) { - subQuery = subQuery.replace('param1', regResult1[0]); - } - expect(regResult1).not.toBe(null); - - expect(secondParams?.val).toBe( - query.filter?.relation?.roles?.createdAt?.ne - ); - - const regResult2 = secondParams?.name.match( - new RegExp(`params_Roles.createdAt_\\d{1,}`) - ); - if (regResult2) { - subQuery = subQuery.replace('param2', regResult2[0]); - } - expect(regResult2).not.toBe(null); - - expect(thirdParams?.val).toBe( - query.filter?.relation?.roles?.createdAt?.nin - ); - const regResult3 = thirdParams?.name.match( - new RegExp(`params_Roles.createdAt_\\d{1,}`) - ); - if (regResult3) { - subQuery = subQuery.replace('param3', regResult3[0]); - } - } - expect(first.alias).toBe(`Users.id`); - expect(first.expression).toBe(`IN ${subQuery}`); - }); - - it('expression for relation other type', () => { - const query = getDefaultQuery(); - query.filter.relation = { - addresses: { - createdAt: { - eq: 'qweqwe', - }, - }, - comments: { - createdAt: { - like: 'sdfsdf', - }, - }, - }; - const firstAlias = 'Addresses.createdAt'; - const secondAlias = 'Comments.createdAt'; - const result = - typeormUtilsServiceUser.getFilterExpressionForRelation(query); - - expect(result.length).toBe(2); - const [first, second] = result; - - const firstResult = first.expression.match( - new RegExp(`params_${firstAlias}_\\d{1,}`) - ); - - if (!firstResult) { - expect(firstResult).not.toBe(null); - throw Error('Should be like pattern'); - } - - expect(first.expression).toBe(`= :${firstResult[0]}`); - expect(first.alias).toBe(`Users__Addresses_addresses.createdAt`); - expect(first.selectInclude).toBe('addresses'); - if (!Array.isArray(first.params)) { - expect(first.params?.name).toBe(`${firstResult[0]}`); - expect(first.params?.val).toBe( - query.filter.relation?.addresses?.createdAt?.eq - ); - } else { - expect(first.params).not.toBeInstanceOf(Array); - } - - const secondResult = second.expression.match( - new RegExp(`params_${secondAlias}_\\d{1,}`) - ); - if (!secondResult) { - expect(secondResult).not.toBe(null); - throw Error('Should be like pattern'); - } - - expect(second.expression).toBe(`ILIKE :${secondResult[0]}`); - expect(second.alias).toBe('Users__Comments_comments.createdAt'); - expect(second.selectInclude).toBe('comments'); - if (!Array.isArray(second.params)) { - expect(second.params?.name).toBe(secondResult[0]); - expect(second.params?.val).toBe( - `%${query.filter.relation?.comments?.createdAt?.like}%` - ); - } else { - expect(second.params).not.toBeInstanceOf(Array); - } - }); - }); - - describe('validateRelationInputData', () => { - let usersData: Users; - beforeEach(async () => { - const result = await userRepository.findOne({ - where: { - id: 1, - }, - relations: { - roles: true, - userGroup: true, - manager: true, - }, - }); - if (!result) { - throw Error('not found mock data'); - } - usersData = result; - }); - it('should be ok', async () => { - const rolesData = usersData.roles.map((i) => ({ - type: 'roles', - id: i.id.toString(), - })); - - const userGroupData = { - type: 'user-groups', - id: usersData.userGroup.id.toString(), - }; - const managerData = { - type: 'users', - id: usersData.manager.id.toString(), - }; - const emptyRoles: { id: string; type: string }[] = []; - const emptyManager = null; - const result = await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - rolesData - ); - const result1 = await typeormUtilsServiceUser.validateRelationInputData( - 'userGroup', - userGroupData - ); - const result2 = await typeormUtilsServiceUser.validateRelationInputData( - 'manager', - managerData - ); - const result3 = await typeormUtilsServiceUser.validateRelationInputData( - 'manager', - emptyManager - ); - const result4 = await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - emptyRoles - ); - expect(result).toEqual(usersData.roles.map((i) => i.id.toString())); - expect(result1).toEqual(usersData.userGroup.id.toString()); - expect(result2).toEqual(usersData.manager.id.toString()); - expect(result3).toEqual(emptyManager); - expect(result4).toEqual(emptyRoles); - }); - - it('Should be error incorrect type name', async () => { - const rolesData = usersData.roles.map((i, index) => ({ - type: index === 1 ? 'other' : 'roles', - id: i.id.toString(), - })) as PostRelationshipData; - - const userGroupData = { - type: 'userGroups', - id: usersData.userGroup.id.toString(), - }; - const managerData = { - type: 'user', - id: usersData.manager.id.toString(), - }; - expect.assertions(3); - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - rolesData - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'userGroup', - userGroupData - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'manager', - managerData - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - }); - - it('Should be error, Incorrect relation type', async () => { - expect.assertions(2); - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - {} as any - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'userGroup', - [] as any - ); - } catch (e) { - expect(e).toBeInstanceOf(UnprocessableEntityException); - } - }); - - it('Should be error, Not fond', async () => { - const rolesData = usersData.roles.map((i, index) => ({ - type: 'roles', - id: index === 1 ? '1000' : i.id.toString(), - })) as PostRelationshipData; - expect.assertions(2); - try { - await typeormUtilsServiceUser.validateRelationInputData( - 'roles', - rolesData - ); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - try { - await typeormUtilsServiceUser.validateRelationInputData('userGroup', { - type: 'user-groups', - id: '10000', - }); - } catch (e) { - expect(e).toBeInstanceOf(NotFoundException); - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts b/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts deleted file mode 100644 index 846a575e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mixin/service/typeorm-utils.service.ts +++ /dev/null @@ -1,678 +0,0 @@ -import { - BadRequestException, - Inject, - Injectable, - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; -import { EntityMetadata, Equal, In, Repository } from 'typeorm'; -import { RelationMetadata as TypeOrmRelationMetadata } from 'typeorm/metadata/RelationMetadata'; - -import { - Entity, - EntityRelation, - EXPRESSION, - FilterOperand, - OperandMapExpressionForNull, - OperandsMapExpression, - OperandsMapExpressionForNullRelation, - ValidateQueryError, -} from '../../types'; -import { - camelToKebab, - getEntityName, - kebabToCamel, - ObjectTyped, - snakeToCamel, -} from '../../helper/utils'; -import { PatchData, PostData, Query } from '../../helper/zod'; -import { CURRENT_ENTITY_REPOSITORY } from '../../constants'; -import { TupleOfEntityRelation } from '../../helper/orm'; - -type RelationAlias = { - [K in TupleOfEntityRelation[number]]: string; -}; -type RelationMetadata = { - [K in TupleOfEntityRelation[number]]: TypeOrmRelationMetadata; -}; - -type ResultQueryExpressionObject = { name: string; val: string }; -type ResultQueryExpressionArray = { name: string; val: string }[]; - -export type RelationshipsResult = { - [K in EntityRelation]: E[K] extends E[K][] ? E[K] : E[K] | null; -}; - -export type ResultQueryExpression = { - alias?: string; - expression: string; - paramsForResult?: string[]; - params?: ResultQueryExpressionObject | ResultQueryExpressionArray; - selectInclude?: string; -}; -export type InputValidateData = { - type: string; - id: string; -}; - -export type ValidateReturn = T extends unknown[] - ? string[] - : T extends null - ? null - : string; - -function isTargetField( - relationField: TupleOfEntityRelation, - field: any -): field is TupleOfEntityRelation[number] { - return relationField.includes(field); -} - -function isRelationField( - relationField: TupleOfEntityRelation, - field: any -): asserts field is EntityRelation { - if (isTargetField(relationField, field)) return; - const error: ValidateQueryError = { - code: 'unrecognized_keys', - path: ['data', 'relationships'], - message: `Resource for relation '${field.toString()}' does not exist`, - keys: [field], - }; - - throw new BadRequestException([error]); -} - -Injectable(); -export class TypeormUtilsService { - private readonly _currentAlias!: string; - private readonly _relationMetadata = {} as RelationMetadata; - private readonly _relationAlias = {} as RelationAlias; - private readonly _relationFields!: TupleOfEntityRelation; - private readonly _entityMetadata!: EntityMetadata; - private _number = 0; - - constructor( - @Inject(CURRENT_ENTITY_REPOSITORY) private repository: Repository - ) { - this._currentAlias = snakeToCamel(repository.metadata.name); - const relationFields = []; - for (const metadata of repository.metadata.relations) { - const propertyName = - metadata.propertyName as TupleOfEntityRelation[number]; - this._relationMetadata[propertyName] = metadata; - this._relationAlias[propertyName] = snakeToCamel( - metadata.inverseEntityMetadata.name - ); - relationFields.push(propertyName); - } - this._relationFields = relationFields as TupleOfEntityRelation; - this._entityMetadata = repository.metadata; - } - get currentAlias() { - return this._currentAlias; - } - - get relationFields() { - return this._relationFields; - } - - relationName(relName: TupleOfEntityRelation[number]) { - return this._relationAlias[relName]; - } - - get currentPrimaryColumn(): keyof E { - return this._entityMetadata.primaryColumns[0].propertyName as keyof E; - } - - getAliasForRelation(relName: TupleOfEntityRelation[number]) { - return `${this.currentAlias}__${this._relationAlias[relName]}_${relName}`; - } - - getRelMetaDataForRelation(relName: TupleOfEntityRelation[number]) { - return this._relationMetadata[relName]; - } - - getPrimaryColumnForRel(relName: TupleOfEntityRelation[number]) { - return this._relationMetadata[relName].inverseEntityMetadata - .primaryColumns[0].propertyName; - } - - private getFilterObject(query: Query, filterType: 'target' | 'relation') { - const { filter } = query; - if (!filter) return null; - return filter[filterType]; - } - - private get number() { - if (this._number > 1000) { - this._number = 0; - } - this._number++; - return this._number; - } - - private getParamName(fieldName: string) { - return `params_${fieldName}_${this.number}`; - } - - getAliasPath(fieldName: unknown): string; - getAliasPath( - fieldName: unknown, - relname: TupleOfEntityRelation[number], - separator?: string - ): string; - getAliasPath(fieldName: unknown, relname: string, separator?: string): string; - getAliasPath( - fieldName: unknown, - relname?: TupleOfEntityRelation[number] | string, - separator = '.' - ): string { - const alias = relname - ? this._relationAlias[relname] || relname - : this.currentAlias; - return `${alias}${separator}${fieldName}`; - } - - private getSubQueryForManyToMany( - fieldName: TupleOfEntityRelation[number], - expression?: string[] - ): string { - const metadataRelation: TypeOrmRelationMetadata = - this._relationMetadata[fieldName]; - const relationPrimaryColumn = - metadataRelation.inverseEntityMetadata.primaryColumns[0].propertyName; - const { joinTableName, inverseJoinColumns, joinColumns } = - metadataRelation.isManyToManyOwner - ? metadataRelation - : metadataRelation.inverseRelation || metadataRelation; - - const { databaseName: queryJoinPropsName } = - metadataRelation.isManyToManyOwner - ? inverseJoinColumns[0] - : joinColumns[0]; - const { databaseName: selectJoinPropsName } = - metadataRelation.isManyToManyOwner - ? joinColumns[0] - : inverseJoinColumns[0]; - - const alias = this.getAliasPath( - this._relationAlias[fieldName], - this.currentAlias, - '-' - ); - - const selectAlias = this.getAliasPath(selectJoinPropsName, alias); - - const query = this.repository - .createQueryBuilder() - .subQuery() - .select(selectAlias) - .from(joinTableName, alias) - .leftJoin( - this._relationMetadata[fieldName].inverseEntityMetadata.target, - this.getAliasForRelation(fieldName), - `${this.getAliasPath(queryJoinPropsName, alias)} = ${this.getAliasPath( - relationPrimaryColumn, - this.getAliasForRelation(fieldName) - )}` - ); - if (!expression) { - query.where( - `${selectAlias} = ${this.getAliasPath(this.currentPrimaryColumn)}` - ); - } else { - for (const i in expression) { - query[i === '0' ? 'where' : 'andWhere'](expression[i]); - } - } - return query.getQuery(); - } - - getFilterExpressionForTarget(query: Query): ResultQueryExpression[] { - const resultExpression: ResultQueryExpression[] = []; - const filterTarget = this.getFilterObject(query, 'target'); - if (!filterTarget) return resultExpression; - for (const [fieldName, filter] of ObjectTyped.entries(filterTarget)) { - if (!filter) continue; - for (const entries of ObjectTyped.entries(filter)) { - const [operand, value] = entries as [FilterOperand, string]; - const valueConditional = - operand === FilterOperand.like ? `%${value}%` : value; - const fieldWithAlias = this.getAliasPath(fieldName); - const paramsName = this.getParamName(fieldWithAlias); - - if (!isTargetField(this._relationFields, fieldName)) { - if ( - (operand === FilterOperand.ne || operand === FilterOperand.eq) && - (valueConditional === 'null' || valueConditional === null) - ) { - const expression = OperandMapExpressionForNull[operand].replace( - EXPRESSION, - paramsName - ); - resultExpression.push({ - alias: fieldWithAlias, - expression, - }); - continue; - } - - const expression = OperandsMapExpression[operand].replace( - EXPRESSION, - paramsName - ); - resultExpression.push({ - alias: fieldWithAlias, - expression, - params: { - val: valueConditional, - name: paramsName, - }, - }); - continue; - } - - const metadataRelation: TypeOrmRelationMetadata = - this._relationMetadata[fieldName]; - const relationTarget = metadataRelation.inverseEntityMetadata.target; - const relationAlias = this._relationAlias[fieldName]; - const subQuery = this.repository.createQueryBuilder().subQuery(); - - const resultOperand = - operand === FilterOperand.eq ? operand : FilterOperand.ne; - switch (metadataRelation.relationType) { - case 'many-to-many': { - const subQuerySql = this.getSubQueryForManyToMany(fieldName); - - const resultOperand = - operand === FilterOperand.eq ? operand : FilterOperand.ne; - - const expression = OperandsMapExpressionForNullRelation[ - resultOperand - ].replace(EXPRESSION, subQuerySql); - - resultExpression.push({ - expression, - }); - break; - } - case 'one-to-many': { - const joinColumn = metadataRelation.inverseSidePropertyPath; - - const aliasPath = this.getAliasPath(joinColumn, fieldName); - const subQuerySql = subQuery - .select(aliasPath, joinColumn) - .from(relationTarget, relationAlias) - .where( - `${aliasPath} = ${this.getAliasPath(this.currentPrimaryColumn)}` - ) - .getQuery(); - - const expression = OperandsMapExpressionForNullRelation[ - resultOperand - ].replace(EXPRESSION, subQuerySql); - - resultExpression.push({ - expression, - }); - break; - } - default: { - const expression = OperandMapExpressionForNull[ - resultOperand - ].replace(EXPRESSION, paramsName); - resultExpression.push({ - alias: fieldWithAlias, - expression, - }); - } - } - } - } - - return resultExpression; - } - - getFilterExpressionForRelation(query: Query): ResultQueryExpression[] { - const resultExpression: ResultQueryExpression[] = []; - const filterRelation = this.getFilterObject(query, 'relation'); - if (!filterRelation) return resultExpression; - - for (const [relationField, propsFilter] of ObjectTyped.entries( - filterRelation - )) { - if (!propsFilter) continue; - if (!isTargetField(this._relationFields, relationField)) continue; - const metadataRelation: TypeOrmRelationMetadata = - this._relationMetadata[relationField]; - - const conditionalForManyToMany: { - conditional: string; - params: { name: string; val: string } | undefined; - }[] = []; - - for (const [relationFieldProps, filter] of ObjectTyped.entries( - propsFilter - )) { - if (!filter) continue; - - for (const entries of ObjectTyped.entries(filter)) { - const [operand, value] = entries as [FilterOperand, string]; - const currentValue = - operand === FilterOperand.like ? `%${value}%` : value; - - const paramsName = this.getParamName( - this.getAliasPath(relationFieldProps.toString(), relationField) - ); - let expression: string; - if (value === 'null') { - const currentOperand = - operand === FilterOperand.eq - ? FilterOperand.eq - : FilterOperand.ne; - expression = OperandMapExpressionForNull[currentOperand]; - } else { - expression = OperandsMapExpression[operand].replace( - EXPRESSION, - paramsName - ); - } - - const params = - value === 'null' - ? undefined - : { - val: currentValue, - name: paramsName, - }; - - switch (metadataRelation.relationType) { - case 'many-to-many': { - conditionalForManyToMany.push({ - params, - conditional: `${this.getAliasPath( - relationFieldProps.toString(), - this.getAliasForRelation(relationField) - )} ${expression}`, - }); - - break; - } - default: { - resultExpression.push({ - alias: this.getAliasPath( - relationFieldProps.toString(), - this.getAliasForRelation(relationField) - ), - expression, - selectInclude: relationField, - params, - }); - } - } - } - } - - if (conditionalForManyToMany.length) { - const expression = conditionalForManyToMany.map((i) => i.conditional); - const subQuery = this.getSubQueryForManyToMany( - relationField, - expression - ); - - const mainExpression = `IN ${subQuery}`; - - const params = conditionalForManyToMany - .filter((i) => i.params) - .map((i) => i.params) as { name: string; val: string }[]; - resultExpression.push({ - alias: this.getAliasPath(this.currentPrimaryColumn), - expression: mainExpression, - paramsForResult: expression, - params, - }); - } - } - return resultExpression; - } - - private throwError(message: string, path: string[], key?: string) { - const error: ValidateQueryError = { - code: 'unrecognized_keys', - path, - message, - }; - if (key) { - error.keys = [key]; - } - throw new BadRequestException([error]); - } - - async *asyncIterateFindRelationships( - relationships: NonNullable< - PatchData['relationships'] | PostData['relationships'] - > - ): AsyncGenerator> { - for (const entries of ObjectTyped.entries(relationships)) { - const [props, data] = entries; - - isRelationField(this._relationFields, props); - if (data === undefined) continue; - - if (data === null) { - yield { [props]: null } as RelationshipsResult; - continue; - } - const isArray = Array.isArray(data); - if (isArray && data.length === 0) { - yield { [props]: [] } as RelationshipsResult; - continue; - } - const condition = isArray ? In(data.map((i) => i.id)) : Equal(data['id']); - const relationsTypeName = kebabToCamel( - isArray ? data[0]['type'] : data['type'] - ); - const primaryField = this.getPrimaryColumnForRel( - props as TupleOfEntityRelation[number] - ); - const relationsTarget = - this._relationMetadata[props as TupleOfEntityRelation[number]] - .inverseEntityMetadata.target; - const result = await this.repository.manager - .getRepository(relationsTarget) - .find({ - select: { - [primaryField]: true, - }, - where: { - [primaryField]: condition, - }, - }); - - if ( - (isArray && (result.length === 0 || data.length !== result.length)) || - (!isArray && result.length === 0) - ) { - const message = isArray - ? `Resource '${relationsTypeName}' with ids '${data - .map((i) => i.id) - .filter((i) => !result.find((r) => r[primaryField] == i)) - .join(',')}' does not exist` - : `Resource '${relationsTypeName}' with id '${data.id}' does not exist`; - - const error: ValidateQueryError = { - code: 'invalid_arguments', - path: ['data', 'relationships', props.toString()], - message, - }; - - throw new BadRequestException([error]); - } - - yield { [props]: isArray ? result : result[0] } as RelationshipsResult; - } - } - - async saveEntityData( - target: E, - relationships: PatchData['relationships'] | PostData['relationships'] - ): Promise { - if (relationships) { - for await (const item of this.asyncIterateFindRelationships( - relationships - )) { - const [props, type] = ObjectTyped.entries(item)[0]; - if (type !== null) { - target[props] = type as any; - } else { - target[props] = null as any; - } - } - } - const saveData = await this.repository.save(target); - let saveDataWithRelation: E | null = null; - if (relationships) { - const queryBuilder = this.repository - .createQueryBuilder(this.currentAlias) - .where({ - [this.currentPrimaryColumn]: Equal( - saveData[this.currentPrimaryColumn] - ), - }); - - for (const [props] of ObjectTyped.entries(relationships)) { - const currentIncludeAlias = this.getAliasForRelation(props.toString()); - - queryBuilder.leftJoinAndSelect( - this.getAliasPath(props), - currentIncludeAlias - ); - } - - saveDataWithRelation = await queryBuilder.getOne(); - } - - return saveDataWithRelation ? saveDataWithRelation : saveData; - } - - async validateRelationInputData< - Rel extends EntityRelation, - In extends InputValidateData | InputValidateData[] - >(rel: Rel, inputData: In): Promise>; - async validateRelationInputData< - Rel extends EntityRelation, - In extends InputValidateData | InputValidateData[] - >(rel: Rel, inputData: In): Promise>; - async validateRelationInputData< - Rel extends EntityRelation, - In extends null | InputValidateData | InputValidateData[] - >(rel: Rel, inputData: In): Promise>; - async validateRelationInputData< - Rel extends EntityRelation, - In extends null | InputValidateData | InputValidateData[] - >(rel: Rel, inputData: In): Promise> { - const relationMetadata = - this._relationMetadata[rel as TupleOfEntityRelation[number]]; - const isArray = Array.isArray(inputData); - - if ( - ['one-to-many', 'many-to-many'].includes(relationMetadata.relationType) && - !isArray - ) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - path: ['data'], - message: 'Body data should be array', - }; - - throw new UnprocessableEntityException([error]); - } - - if ( - ['one-to-one', 'many-to-one'].includes(relationMetadata.relationType) && - isArray - ) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - path: ['data'], - message: 'Body data should be object', - }; - - throw new UnprocessableEntityException([error]); - } - if (inputData === null) { - const result = null; - return result as ValidateReturn; - } - if (isArray && inputData.length === 0) { - const result: any[] = []; - return result as ValidateReturn; - } - - const prepareData = isArray ? inputData : [inputData]; - - const errors: ValidateQueryError[] = []; - let i = 0; - const typeName = camelToKebab( - getEntityName(relationMetadata.inverseEntityMetadata.target) - ); - - for (const prepareItem of prepareData) { - if (prepareItem.type !== typeName) { - const path = isArray ? ['data', i.toString()] : ['data']; - errors.push({ - code: 'invalid_arguments', - path: path, - message: `Type should be equal to type of relName: "${rel.toString()}". Type of ${rel.toString()} is "${typeName}" but receive - "${ - prepareItem.type - }"`, - }); - } - i++; - } - if (errors.length) { - throw new UnprocessableEntityException(errors); - } - - const checkResult = await this.repository.manager - .getRepository(relationMetadata.inverseEntityMetadata.target) - .find({ - select: { - [this.getPrimaryColumnForRel(rel.toString())]: true, - }, - where: { - [this.getPrimaryColumnForRel(rel.toString())]: In( - prepareData.map((i) => i.id) - ), - }, - }); - - if (checkResult.length === prepareData.length) { - return ( - isArray ? inputData.map((i) => i.id) : inputData.id - ) as ValidateReturn; - } - - const resulDataMap = checkResult.reduce((acum, item) => { - acum[item[this.getPrimaryColumnForRel(rel.toString())]] = true; - return acum; - }, {} as Record); - - i = 0; - for (const item of prepareData) { - if (!resulDataMap[item.id]) { - const path = isArray ? ['data', i.toString(), 'id'] : ['data', 'id']; - errors.push({ - code: 'invalid_arguments', - path: path, - message: `Not exist item "${ - item.id - }" in relation "${rel.toString()}"`, - }); - } - i++; - } - throw new NotFoundException(errors); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test b/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test deleted file mode 100644 index fa08bc14..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test +++ /dev/null @@ -1,647 +0,0 @@ --- --- PostgreSQL database dump --- - --- Dumped from database version 12.5 --- Dumped by pg_dump version 12.5 - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -create extension "uuid-ossp"; - --- --- Name: comment_kind_enum; Type: TYPE; Schema: public; Owner: - --- - -CREATE TYPE public.comment_kind_enum AS ENUM ( - 'COMMENT', - 'MESSAGE', - 'NOTE' -); - - -SET default_table_access_method = heap; - --- --- Name: addresses; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.addresses ( - id integer NOT NULL, - city character varying(70) DEFAULT NULL::character varying, - state character varying(70) DEFAULT NULL::character varying, - country character varying(70) DEFAULT NULL::character varying, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - array_field text[] -); - - --- --- Name: addresses_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.addresses_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: addresses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.addresses_id_seq OWNED BY public.addresses.id; - - --- --- Name: comments; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.comments ( - id integer NOT NULL, - text text NOT NULL, - kind public.comment_kind_enum NOT NULL, - created_by integer, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: comments_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.comments_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.comments_id_seq OWNED BY public.comments.id; - - --- --- Name: notes; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.notes ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - text text NOT NULL, - created_by integer, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - - --- --- Name: migrations; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.migrations ( - id integer NOT NULL, - "timestamp" bigint NOT NULL, - name character varying NOT NULL -); - - --- --- Name: migrations_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.migrations_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: migrations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.migrations_id_seq OWNED BY public.migrations.id; - - --- --- Name: pods; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.pods ( - id integer NOT NULL, - name character varying, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: pods_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.pods_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: pods_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.pods_id_seq OWNED BY public.pods.id; - - --- --- Name: requests; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.requests ( - id integer NOT NULL, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: requests_have_pod_locks; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.requests_have_pod_locks ( - id integer NOT NULL, - request_id integer NOT NULL, - pod_id integer NOT NULL, - external_id integer, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: requests_have_pod_locks_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.requests_have_pod_locks_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: requests_have_pod_locks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.requests_have_pod_locks_id_seq OWNED BY public.requests_have_pod_locks.id; - - --- --- Name: requests_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.requests_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: requests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.requests_id_seq OWNED BY public.requests.id; - - --- --- Name: roles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.roles ( - id integer NOT NULL, - name character varying(128) DEFAULT NULL::character varying, - key character varying(128) NOT NULL, - is_default boolean DEFAULT false, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.roles_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.roles_id_seq OWNED BY public.roles.id; - - --- --- Name: typeorm_metadata; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.typeorm_metadata ( - type character varying NOT NULL, - database character varying, - schema character varying, - "table" character varying, - name character varying, - value text -); - - --- --- Name: users; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.users ( - id integer NOT NULL, - login character varying(100) NOT NULL, - first_name character varying, - last_name character varying, - is_active boolean DEFAULT false, - test_real real[], - test_array_null real[], - manager_id integer, - addresses_id integer, - user_groups_id integer, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - test_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - --- --- Name: user_groups; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.user_groups ( - id integer NOT NULL, - label character varying NOT NULL -); - - --- --- Name: users_have_roles; Type: TABLE; Schema: public; Owner: - --- - -CREATE TABLE public.users_have_roles ( - id integer NOT NULL, - user_id integer NOT NULL, - role_id integer NOT NULL, - created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, - updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP -); - - --- --- Name: users_have_roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.users_have_roles_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: users_have_roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.users_have_roles_id_seq OWNED BY public.users_have_roles.id; - - --- --- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.users_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - - --- --- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; - - --- --- Name: user_groups_id_seq; Type: SEQUENCE; Schema: public; Owner: - --- - -CREATE SEQUENCE public.user_groups_id_seq - AS integer - START WITH 1 - INCREMENT BY 1 - NO MINVALUE - NO MAXVALUE - CACHE 1; - --- --- Name: user_groups_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - --- - -ALTER SEQUENCE public.user_groups_id_seq OWNED BY public.user_groups.id; - - --- --- Name: addresses id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.addresses ALTER COLUMN id SET DEFAULT nextval('public.addresses_id_seq'::regclass); - - --- --- Name: comments id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comments ALTER COLUMN id SET DEFAULT nextval('public.comments_id_seq'::regclass); - - --- --- Name: migrations id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migrations ALTER COLUMN id SET DEFAULT nextval('public.migrations_id_seq'::regclass); - - --- --- Name: pods id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.pods ALTER COLUMN id SET DEFAULT nextval('public.pods_id_seq'::regclass); - - --- --- Name: requests id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests ALTER COLUMN id SET DEFAULT nextval('public.requests_id_seq'::regclass); - - --- --- Name: requests_have_pod_locks id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests_have_pod_locks ALTER COLUMN id SET DEFAULT nextval('public.requests_have_pod_locks_id_seq'::regclass); - - --- --- Name: roles id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.roles ALTER COLUMN id SET DEFAULT nextval('public.roles_id_seq'::regclass); - - --- --- Name: users id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); - - --- --- Name: users id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.user_groups ALTER COLUMN id SET DEFAULT nextval('public.user_groups_id_seq'::regclass); - - --- --- Name: users_have_roles id; Type: DEFAULT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users_have_roles ALTER COLUMN id SET DEFAULT nextval('public.users_have_roles_id_seq'::regclass); - - --- --- Name: requests PK_0428f484e96f9e6a55955f29b5f; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests - ADD CONSTRAINT "PK_0428f484e96f9e6a55955f29b5f" PRIMARY KEY (id); - - --- --- Name: addresses PK_745d8f43d3af10ab8247465e450; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.addresses - ADD CONSTRAINT "PK_745d8f43d3af10ab8247465e450" PRIMARY KEY (id); - - --- --- Name: comments PK_8bf68bc960f2b69e818bdb90dcb; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comments - ADD CONSTRAINT "PK_8bf68bc960f2b69e818bdb90dcb" PRIMARY KEY (id); - - --- --- Name: migrations PK_8c82d7f526340ab734260ea46be; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.migrations - ADD CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id); - - --- --- Name: users_have_roles PK_9bb88c2f9f64bff7570e4108108; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users_have_roles - ADD CONSTRAINT "PK_9bb88c2f9f64bff7570e4108108" PRIMARY KEY (id); - - --- --- Name: users PK_a3ffb1c0c8416b9fc6f907b7433; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY (id); - - --- --- Name: users PK_user_groups; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.user_groups - ADD CONSTRAINT "PK_user_groups" PRIMARY KEY (id); - --- --- Name: pods PK_b00bbc2c7fb41627be2b169f0dd; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.pods - ADD CONSTRAINT "PK_b00bbc2c7fb41627be2b169f0dd" PRIMARY KEY (id); - - --- --- Name: roles PK_c1433d71a4838793a49dcad46ab; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.roles - ADD CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY (id); - - --- --- Name: requests_have_pod_locks PK_f214657396a396b70a697b04a85; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests_have_pod_locks - ADD CONSTRAINT "PK_f214657396a396b70a697b04a85" PRIMARY KEY (id); - - --- --- Name: users UQ_2d443082eccd5198f95f2a36e2c; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "UQ_2d443082eccd5198f95f2a36e2c" UNIQUE (login); - - --- --- Name: roles UQ_a87cf0659c3ac379b339acf36a2; Type: CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.roles - ADD CONSTRAINT "UQ_a87cf0659c3ac379b339acf36a2" UNIQUE (key); - - --- --- Name: IDX_48d6a9a1ab3943e6c6d2a25d2e; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX "IDX_48d6a9a1ab3943e6c6d2a25d2e" ON public.requests_have_pod_locks USING btree (request_id, pod_id); - - --- --- Name: IDX_61c360686dfe8d62a9b03873bf; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX "IDX_61c360686dfe8d62a9b03873bf" ON public.users_have_roles USING btree (user_id, role_id); - - --- --- Name: users FK_2f8d527df0d3acb8aa51945a968; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "FK_2f8d527df0d3acb8aa51945a968" FOREIGN KEY (addresses_id) REFERENCES public.addresses(id); - - --- --- Name: users FK_user_groups; Type: FK CONSTRAINT; Schema: public; Owner: - --- -ALTER TABLE ONLY public.users - ADD CONSTRAINT "FK_user_groups" FOREIGN KEY (user_groups_id) REFERENCES public.user_groups(id); - - --- --- Name: users_have_roles FK_6e768e03083247102b401b74b46; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users_have_roles - ADD CONSTRAINT "FK_6e768e03083247102b401b74b46" FOREIGN KEY (role_id) REFERENCES public.roles(id); - - --- --- Name: comments FK_980bfefe00ed11685f325d0bd4c; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.comments - ADD CONSTRAINT "FK_980bfefe00ed11685f325d0bd4c" FOREIGN KEY (created_by) REFERENCES public.users(id); - - --- --- Name: notes FK_980bfefe00ed11685f325d0bd4c; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.notes - ADD CONSTRAINT "FK_notes" FOREIGN KEY (created_by) REFERENCES public.users(id); - - --- --- Name: requests_have_pod_locks FK_c7531fe6bbb926bba3f69fcbb55; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests_have_pod_locks - ADD CONSTRAINT "FK_c7531fe6bbb926bba3f69fcbb55" FOREIGN KEY (pod_id) REFERENCES public.pods(id); - - --- --- Name: users_have_roles FK_df6a0246fcd887dd8ffeed2c292; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users_have_roles - ADD CONSTRAINT "FK_df6a0246fcd887dd8ffeed2c292" FOREIGN KEY (user_id) REFERENCES public.users(id); - - --- --- Name: requests_have_pod_locks FK_f3729b493fcdb7309cad08837ff; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.requests_have_pod_locks - ADD CONSTRAINT "FK_f3729b493fcdb7309cad08837ff" FOREIGN KEY (request_id) REFERENCES public.requests(id); - - --- --- Name: users FK_fba2d8e029689aa8fea98e53c91; Type: FK CONSTRAINT; Schema: public; Owner: - --- - -ALTER TABLE ONLY public.users - ADD CONSTRAINT "FK_fba2d8e029689aa8fea98e53c91" FOREIGN KEY (manager_id) REFERENCES public.users(id); - - --- --- PostgreSQL database dump complete --- diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts deleted file mode 100644 index 3f0c7dab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - PrimaryGeneratedColumn, - OneToOne, - Column, - Entity, - UpdateDateColumn, -} from 'typeorm'; - -import { Users, IUsers } from './index'; - -export type IAddresses = Addresses; - -@Entity('addresses') -export class Addresses { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'varchar', - length: 70, - nullable: true, - default: 'NULL', - }) - public city!: string; - - @Column({ - type: 'varchar', - length: 70, - nullable: true, - default: 'NULL', - }) - public state!: string; - - @Column({ - type: 'varchar', - length: 68, - nullable: true, - default: 'NULL', - }) - public country!: string; - - @Column({ - name: 'array_field', - type: 'varchar', - nullable: true, - default: 'NULL', - array: true, - }) - public arrayField!: string[]; - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @OneToOne(() => Users, (item) => item.addresses) - public user!: IUsers; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts deleted file mode 100644 index c6f3ff8e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { - PrimaryGeneratedColumn, - Column, - Entity, - JoinColumn, - ManyToOne, - UpdateDateColumn, -} from 'typeorm'; - -export enum CommentKind { - Comment = 'COMMENT', - Message = 'MESSAGE', - Note = 'NOTE', -} - -import { Users, IUsers } from './index'; - -@Entity('comments') -export class Comments { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'text', - nullable: false, - }) - public text!: string; - - @Column({ - type: 'enum', - enum: CommentKind, - nullable: false, - }) - public kind!: CommentKind; - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToOne(() => Users, (item) => item.id) - @JoinColumn({ - name: 'created_by', - }) - public createdBy!: IUsers; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts deleted file mode 100644 index ba42e083..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -export * from './users'; -export * from './roles'; -export * from './requests-have-pod-locks'; -export * from './requests'; -export * from './pods'; -export * from './comments'; -export * from './addresses'; -export * from './user-groups'; -export * from './notes'; - -import { Users } from './users'; -import { Roles } from './roles'; -import { Requests } from './requests'; -import { Pods } from './pods'; -import { Comments } from './comments'; -import { Addresses } from './addresses'; -import { UserGroups } from './user-groups'; -import { Notes } from './notes'; - -export const Entities = [ - Users, - Roles, - Requests, - Pods, - Comments, - Addresses, - UserGroups, - Notes, -]; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts deleted file mode 100644 index e8694aca..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - PrimaryGeneratedColumn, - Column, - Entity, - JoinColumn, - ManyToOne, - UpdateDateColumn, -} from 'typeorm'; - -import { Users, IUsers } from './index'; - -@Entity('notes') -export class Notes { - @PrimaryGeneratedColumn('uuid') - public id!: string; - - @Column({ - type: 'text', - nullable: false, - }) - public text!: string; - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToOne(() => Users, (item) => item.notes) - @JoinColumn({ - name: 'created_by', - }) - public createdBy!: IUsers; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts deleted file mode 100644 index b5fb898f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - Column, - CreateDateColumn, - Entity, - ManyToMany, - PrimaryColumn, - UpdateDateColumn, -} from 'typeorm'; - -import { IRequests, Requests } from './index'; - -export type IPods = Pods; - -@Entity('pods') -export class Pods { - @PrimaryColumn() - public id!: string; - - @Column({ - type: 'varchar', - length: 50, - nullable: false, - unique: true, - }) - public name!: string; - - @CreateDateColumn({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToMany(() => Requests, (item) => item.podLocks) - public lockedRequests!: IRequests[]; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts deleted file mode 100644 index 7f1f8125..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - AfterLoad, - BeforeInsert, - BeforeRemove, - BeforeUpdate, - Column, - CreateDateColumn, - Entity, - PrimaryGeneratedColumn, - UpdateDateColumn, -} from 'typeorm'; - -export type IRequestsHavePodLocks = RequestsHavePodLocks; - -@Entity('requests_have_pod_locks') -export class RequestsHavePodLocks { - @PrimaryGeneratedColumn() - public id!: number; - - @AfterLoad() - protected getRequestId() { - this.requestId = this.request_id; - } - - @BeforeInsert() - @BeforeUpdate() - @BeforeRemove() - protected setRequestId() { - if (this.requestId) { - this.request_id = this.requestId; - } - } - - public requestId!: number; - - @AfterLoad() - protected getPodId() { - this.podId = this.pod_id; - } - - @BeforeInsert() - @BeforeUpdate() - @BeforeRemove() - protected setPodId() { - if (this.podId) { - this.pod_id = this.podId; - } - } - - public podId!: number; - - @Column({ - name: 'request_id', - type: 'int', - nullable: false, - }) - protected request_id!: number; - - @Column({ - name: 'pod_id', - type: 'int', - nullable: false, - }) - protected pod_id!: number; - - @CreateDateColumn({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @Column({ - name: 'external_id', - type: 'int', - nullable: true, - unsigned: true, - default: 'NULL', - unique: true, - }) - public externalId!: number; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts deleted file mode 100644 index 9bd64266..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - PrimaryGeneratedColumn, - Entity, - CreateDateColumn, - UpdateDateColumn, - ManyToMany, - JoinTable, -} from 'typeorm'; - -import { Pods, IPods } from './index'; - -export type IRequests = Requests; - -@Entity('requests') -export class Requests { - @PrimaryGeneratedColumn() - public id!: number; - - @CreateDateColumn({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToMany(() => Pods, (item) => item.lockedRequests) - @JoinTable({ - name: 'requests_have_pod_locks', - inverseJoinColumn: { - referencedColumnName: 'id', - name: 'pod_id', - }, - joinColumn: { - referencedColumnName: 'id', - name: 'request_id', - }, - }) - public podLocks!: IPods[]; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts deleted file mode 100644 index 4689628a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { - PrimaryGeneratedColumn, - Entity, - Column, - ManyToMany, - UpdateDateColumn, -} from 'typeorm'; - -import { Users, IUsers } from './index'; - -@Entity('roles') -export class Roles { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'varchar', - length: 128, - nullable: true, - default: 'NULL', - }) - public name!: string; - - @Column({ - type: 'varchar', - length: 128, - nullable: false, - unique: true, - }) - public key!: string; - - @Column({ - name: 'is_default', - type: 'boolean', - default: 'false', - }) - public isDefault!: boolean; - - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @ManyToMany(() => Users, (item) => item.roles) - public users!: IUsers[]; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts deleted file mode 100644 index a6727416..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { PrimaryGeneratedColumn, OneToMany, Entity, Column } from 'typeorm'; - -import { IUsers, Users } from './index'; - -@Entity('user_groups') -export class UserGroups { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'varchar', - length: 50, - nullable: false, - unique: true, - }) - public label!: string; - - @OneToMany(() => Users, (item) => item.userGroup) - public users!: IUsers[]; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts deleted file mode 100644 index bdf61878..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { - PrimaryGeneratedColumn, - ManyToMany, - JoinColumn, - JoinTable, - OneToOne, - OneToMany, - Entity, - Column, - UpdateDateColumn, - ManyToOne, -} from 'typeorm'; - -import { Addresses, Roles, Comments, Notes, UserGroups } from './index'; - -export type IUsers = Users; - -@Entity('users') -export class Users { - @PrimaryGeneratedColumn() - public id!: number; - - @Column({ - type: 'varchar', - length: 100, - nullable: false, - unique: true, - }) - public login!: string; - - @Column({ - name: 'first_name', - type: 'varchar', - length: 100, - nullable: true, - default: 'NULL', - }) - public firstName!: string; - - @Column({ - name: 'test_real', - type: 'real', - array: true, - default: [], - }) - public testReal!: number[]; - - @Column({ - name: 'test_array_null', - type: 'real', - array: true, - nullable: true, - }) - public testArrayNull!: number[] | null; - - @Column({ - name: 'last_name', - type: 'varchar', - length: 100, - nullable: true, - default: 'NULL', - }) - public lastName!: string; - - @Column({ - name: 'is_active', - type: 'boolean', - width: 1, - nullable: true, - default: false, - }) - public isActive!: boolean; - - @Column({ - name: 'created_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public createdAt!: Date; - - @Column({ - name: 'test_date', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public testDate!: Date; - - @UpdateDateColumn({ - name: 'updated_at', - type: 'timestamp', - nullable: true, - default: 'CURRENT_TIMESTAMP', - }) - public updatedAt!: Date; - - @OneToOne(() => Addresses, (item) => item.id) - @JoinColumn({ - name: 'addresses_id', - }) - public addresses!: Addresses; - - @OneToOne(() => Users, (item) => item.id) - @JoinColumn({ - name: 'manager_id', - }) - public manager!: Users; - - @ManyToMany(() => Roles, (item) => item.users) - @JoinTable({ - name: 'users_have_roles', - inverseJoinColumn: { - referencedColumnName: 'id', - name: 'role_id', - }, - joinColumn: { - referencedColumnName: 'id', - name: 'user_id', - }, - }) - public roles!: Roles[]; - - @OneToMany(() => Comments, (item) => item.createdBy) - public comments!: Comments[]; - - @OneToMany(() => Notes, (item) => item.createdBy) - public notes!: Notes[]; - - @ManyToOne(() => UserGroups, (userGroup) => userGroup.id) - @JoinColumn({ name: 'user_groups_id' }) - public userGroup!: UserGroups; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts deleted file mode 100644 index bd6b43f6..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { TypeOrmModule } from '@nestjs/typeorm'; -import { DynamicModule } from '@nestjs/common'; -import { DataType, IMemoryDb, newDb } from 'pg-mem'; -import { readFileSync } from 'fs'; -import { join } from 'path'; - -import { - Users, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - UserGroups, - Notes, -} from './entities'; -import { DataSource } from 'typeorm'; - -import { v4 } from 'uuid'; - -export * from './entities'; -export * from './utils'; - -export const entities = [ - Users, - UserGroups, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - Notes, -]; - -export function createAndPullSchemaBase(): IMemoryDb { - const dump = readFileSync(join(__dirname, 'db-for-test'), { - encoding: 'utf8', - }); - const db = newDb({ - autoCreateForeignKeyIndices: true, - }); - - db.public.registerFunction({ - name: 'current_database', - implementation: () => 'test', - }); - - db.public.registerFunction({ - name: 'version', - implementation: () => - 'PostgreSQL 12.5 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.2.1_pre1) 10.2.1 20201203, 64-bit', - }); - - db.registerExtension('uuid-ossp', (schema) => { - schema.registerFunction({ - name: 'uuid_generate_v4', - returns: DataType.uuid, - implementation: v4, - impure: true, - }); - }); - db.public.none(dump); - return db; -} -export function mockDBTestModule(db: IMemoryDb): DynamicModule { - return TypeOrmModule.forRootAsync({ - useFactory() { - return { - type: 'postgres', - // logging: true, - entities: [ - Users, - UserGroups, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - Notes, - ], - }; - }, - async dataSourceFactory(options) { - const dataSource: DataSource = await db.adapters.createTypeormDataSource( - options - ); - - return dataSource; - }, - }); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts deleted file mode 100644 index 7cb5aa8b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './pull-data'; -export * from './provider-entities'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts deleted file mode 100644 index 4b47577b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { DataSource, Repository } from 'typeorm'; -import { Provider } from '@nestjs/common'; -import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm'; - -import { - Addresses, - Comments, - Entities, - Notes, - Pods, - Roles, - UserGroups, -} from '../entities'; -import { Users } from '../entities'; -import { DEFAULT_CONNECTION_NAME } from '../../constants'; -import { TestingModule } from '@nestjs/testing'; - -export function providerEntities( - dataSourceToken: ReturnType -): Provider[] { - return Entities.map((entitiy) => { - return { - provide: getRepositoryToken(entitiy, DEFAULT_CONNECTION_NAME), - useFactory(dataSource: DataSource) { - return dataSource.getRepository(entitiy); - }, - inject: [getDataSourceToken()], - }; - }); -} - -export function getRepository(module: TestingModule) { - const userRepository = module.get>( - getRepositoryToken(Users, DEFAULT_CONNECTION_NAME) - ); - - const addressesRepository = module.get>( - getRepositoryToken(Addresses, DEFAULT_CONNECTION_NAME) - ); - - const notesRepository = module.get>( - getRepositoryToken(Notes, DEFAULT_CONNECTION_NAME) - ); - - const commentsRepository = module.get>( - getRepositoryToken(Comments, DEFAULT_CONNECTION_NAME) - ); - const rolesRepository = module.get>( - getRepositoryToken(Roles, DEFAULT_CONNECTION_NAME) - ); - - const userGroupRepository = module.get>( - getRepositoryToken(UserGroups, DEFAULT_CONNECTION_NAME) - ); - - const podsRepository = module.get>( - getRepositoryToken(Pods, DEFAULT_CONNECTION_NAME) - ); - - return { - userRepository, - addressesRepository, - notesRepository, - commentsRepository, - rolesRepository, - userGroupRepository, - podsRepository, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts deleted file mode 100644 index f28c693e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { Repository } from 'typeorm'; -import { faker } from '@faker-js/faker'; -import { - Addresses, - CommentKind, - Comments, - Notes, - Roles, - UserGroups, - Users, -} from '../entities'; - -export async function pullAddress(addressRepo: Repository) { - const address = new Addresses(); - address.city = faker.location.city(); - address.country = faker.location.country(); - address.arrayField = [ - faker.string.alphanumeric(5), - faker.string.alphanumeric(5), - ]; - address.state = faker.location.state(); - return addressRepo.save(address); -} - -export async function pullComment(commentRepo: Repository) { - const comment = new Comments(); - comment.text = faker.lorem.paragraph(faker.number.int(5)); - comment.kind = CommentKind.Comment; - return commentRepo.save(comment); -} - -export async function pullNote(noteRepo: Repository) { - const note = new Notes(); - note.text = faker.lorem.paragraph(faker.number.int(5)); - return noteRepo.save(note); -} - -export async function pullRole(roleRepo: Repository) { - const role = new Roles(); - role.key = faker.string.alphanumeric(5); - role.name = faker.string.alphanumeric(5); - return roleRepo.save(role); -} - -export async function pullUser(userPero: Repository) { - const user = new Users(); - user.firstName = faker.person.firstName(); - user.lastName = faker.person.lastName(); - user.isActive = faker.datatype.boolean(); - user.login = faker.internet.userName({ - lastName: user.lastName, - firstName: user.firstName, - }); - user.testReal = [faker.number.float({ fractionDigits: 4 })]; - user.testArrayNull = null; - - user.testDate = faker.date.anytime(); - - return userPero.save(user); -} - -export async function pullUserGroup(userGroupRepo: Repository) { - const userGroup = new UserGroups(); - userGroup.label = faker.string.alphanumeric(5); - return userGroupRepo.save(userGroup); -} - -export async function pullAllData( - userPero: Repository, - addressRepo?: Repository, - noteRepo?: Repository, - commentRepo?: Repository, - roleRepo?: Repository, - userGroupRepo?: Repository -) { - const user = await pullUser(userPero); - if (addressRepo) { - user.addresses = await pullAddress(addressRepo); - } - - if (noteRepo) { - user.notes = [ - await pullNote(noteRepo), - await pullNote(noteRepo), - await pullNote(noteRepo), - ]; - } - - if (commentRepo) { - user.comments = [ - await pullComment(commentRepo), - await pullComment(commentRepo), - await pullComment(commentRepo), - await pullComment(commentRepo), - ]; - } - - if (userGroupRepo) { - await pullUserGroup(userGroupRepo); - await pullUserGroup(userGroupRepo); - await pullUserGroup(userGroupRepo); - user.userGroup = await pullUserGroup(userGroupRepo); - } - - if (roleRepo) { - await pullRole(roleRepo); - await pullRole(roleRepo); - await pullRole(roleRepo); - user.roles = [ - await pullRole(roleRepo), - await pullRole(roleRepo), - await pullRole(roleRepo), - ]; - } - - user.manager = await pullUser(userPero); - await pullUser(userPero); - await pullUser(userPero); - await pullUser(userPero); - await userPero.save(user); - return user; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts deleted file mode 100644 index ec16128c..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { AsyncLocalStorage } from 'async_hooks'; -import { - DynamicModule, - Inject, - MiddlewareConsumer, - Module, - NestModule, -} from '@nestjs/common'; -import { DiscoveryModule } from '@nestjs/core'; - -import { OperationController } from './controllers'; -import { ExplorerService, ExecuteService, SwaggerService } from './service'; - -import { - MapControllerEntity, - MapEntityNameToEntity, - ZodInputOperation, - AsyncIterate, -} from './factory'; -import { ModuleOptions } from '../../types'; -import { MAP_CONTROLLER_INTERCEPTORS, OPTIONS } from './constants'; - -@Module({}) -export class AtomicOperationModule implements NestModule { - static forRoot( - options: ModuleOptions, - entityModules: DynamicModule[], - commonModule: DynamicModule - ): DynamicModule { - return { - module: AtomicOperationModule, - controllers: [OperationController], - providers: [ - ExplorerService, - ExecuteService, - SwaggerService, - AsyncIterate, - MapControllerEntity(options.entities, entityModules), - MapEntityNameToEntity(options.entities), - ZodInputOperation(options.connectionName), - { - provide: MAP_CONTROLLER_INTERCEPTORS, - useValue: new Map(), - }, - { - provide: OPTIONS, - useValue: options.options, - }, - { - provide: AsyncLocalStorage, - useValue: new AsyncLocalStorage(), - }, - ], - imports: [DiscoveryModule, commonModule], - }; - } - @Inject(AsyncLocalStorage) private readonly als!: AsyncLocalStorage; - - configure(consumer: MiddlewareConsumer) { - consumer - .apply((req: any, res: any, next: any) => { - const store = { - req: req, - res: res, - next: next, - }; - this.als.run(store, () => next()); - }) - .forRoutes('*'); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts deleted file mode 100644 index ccace714..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const MAP_CONTROLLER_ENTITY = Symbol('MAP_CONTROLLER_ENTITY'); -export const MAP_CONTROLLER_INTERCEPTORS = Symbol( - 'MAP_CONTROLLER_INTERCEPTORS' -); -export const MAP_ENTITY = Symbol('MAP_ENTITY'); -export const ZOD_INPUT_OPERATION = Symbol('ZOD_INPUT_OPERATION'); -export const ASYNC_ITERATOR_FACTORY = Symbol('ASYNC_ITERATOR_FACTORY'); -export const KEY_MAIN_INPUT_SCHEMA = 'atomic:operations'; -export const KEY_MAIN_OUTPUT_SCHEMA = 'atomic:results'; -export const OPTIONS = Symbol('OPTIONS'); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts deleted file mode 100644 index e81188ae..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './operation.controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts deleted file mode 100644 index af5511e2..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { DiscoveryModule } from '@nestjs/core'; -import { HttpException } from '@nestjs/common'; -import { Module } from '@nestjs/core/injector/module'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { IMemoryDb } from 'pg-mem'; - -import { OperationController } from './operation.controller'; -import { ExecuteService, ExplorerService } from '../service'; -import { InputArray, Operation } from '../utils'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../mock-utils'; - -import { - ASYNC_ITERATOR_FACTORY, - KEY_MAIN_OUTPUT_SCHEMA, - MAP_CONTROLLER_ENTITY, - MAP_ENTITY, - ZOD_INPUT_OPERATION, - MAP_CONTROLLER_INTERCEPTORS, - OPTIONS, -} from '../constants'; - -import { CurrentDataSourceProvider } from '../../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../constants'; -import { OperationMethode } from '../types'; -import { AsyncLocalStorage } from 'async_hooks'; - -describe('OperationController', () => { - let db: IMemoryDb; - let operationController: OperationController; - let explorerService: ExplorerService; - let executeService: ExecuteService; - - beforeEach(async () => { - db = createAndPullSchemaBase(); - const app: TestingModule = await Test.createTestingModule({ - imports: [DiscoveryModule, mockDBTestModule(db)], - controllers: [OperationController], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - ExplorerService, - ExecuteService, - { - provide: MAP_ENTITY, - useValue: {}, - }, - { - provide: MAP_CONTROLLER_ENTITY, - useValue: {}, - }, - { - provide: ASYNC_ITERATOR_FACTORY, - useValue: {}, - }, - { - provide: ZOD_INPUT_OPERATION, - useValue: {}, - }, - { - provide: OPTIONS, - useValue: {}, - }, - { - provide: MAP_CONTROLLER_INTERCEPTORS, - useValue: {}, - }, - { - provide: AsyncLocalStorage, - useValue: new AsyncLocalStorage(), - }, - ], - }).compile(); - - operationController = app.get(OperationController); - explorerService = app.get>(ExplorerService); - executeService = app.get(ExecuteService); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - describe('index', () => { - it('should return the result of executeService.run', async () => { - const inputArrayMock: InputArray = [ - { - ref: { - id: '1', - relationship: 'belongs-to', - type: 'TypeA', - }, - op: Operation.add, - }, - ]; - const paramsForExecuteMock = [ - { - module: new (class Module {})() as Module, - params: [1, 'nameRel', { type: 'name', id: '' }], - methodName: 'patchOne', - controller: JsonBaseController, - }, - ]; - - const mockReturnData = { data: { someData: '' } }; - - const getControllerByEntityNameSpy = jest - .spyOn(explorerService, 'getControllerByEntityName') - .mockReturnValue(paramsForExecuteMock[0].controller); - const getMethodNameByParamSpy = jest - .spyOn(explorerService, 'getMethodNameByParam') - .mockReturnValue( - paramsForExecuteMock[0].methodName as OperationMethode - ); - const getModulesByControllerSpy = jest - .spyOn(explorerService, 'getParamsForMethod') - .mockReturnValue( - paramsForExecuteMock[0].params as Parameters< - JsonBaseController['deleteOne'] - > - ); - const getParamsForMethodSpy = jest - .spyOn(explorerService, 'getModulesByController') - .mockReturnValue(paramsForExecuteMock[0].module); - const runSpy = jest - .spyOn(executeService, 'run') - .mockResolvedValue([mockReturnData] as never); - - expect(await operationController.index(inputArrayMock)).toEqual({ - [KEY_MAIN_OUTPUT_SCHEMA]: [mockReturnData], - }); - - expect(getControllerByEntityNameSpy).toHaveBeenCalledWith('TypeA'); - expect(getMethodNameByParamSpy).toHaveBeenCalledWith( - inputArrayMock[0].op, - inputArrayMock[0].ref.id, - inputArrayMock[0].ref.relationship - ); - expect(getModulesByControllerSpy).toHaveBeenCalledWith( - paramsForExecuteMock[0].methodName, - { op: inputArrayMock[0].op, ref: inputArrayMock[0].ref } - ); - expect(getParamsForMethodSpy).toHaveBeenCalledWith( - paramsForExecuteMock[0].controller - ); - // expect(runSpy).toHaveBeenCalledWith(paramsForExecuteMock[0].module, paramsForExecuteMock[0].methodName, paramsForExecuteMock[0].params); - - expect(runSpy).toHaveBeenCalledWith(paramsForExecuteMock, []); - }); - - it('should throw NotFoundException when type does not exist', async () => { - const inputArrayMock: any[] = [ - { - ref: { - id: '1', - relationship: 'belongs-to', - type: 'TypeA', - }, - op: Operation.add, - }, - ]; - - jest - .spyOn(explorerService, 'getControllerByEntityName') - .mockImplementationOnce(() => { - throw new HttpException('Resource does not exist', 404); - }); - expect.assertions(1); - try { - await operationController.index(inputArrayMock as InputArray); - } catch (e) { - expect(e).toBeInstanceOf(HttpException); - } - }); - - it('should throw MethodNotAllowedException when operation not allowed', async () => { - const inputArrayMock = [ - { - ref: { - id: '1', - relationship: 'belongs-to', - type: 'TypeA', - }, - op: Operation.add, - }, - ]; - - jest - .spyOn(explorerService, 'getControllerByEntityName') - .mockReturnValue(Promise.resolve({}) as any); - - jest - .spyOn(explorerService, 'getMethodNameByParam') - .mockImplementationOnce(() => { - throw new HttpException('Operation not allowed', 405); - }); - - expect.assertions(1); - try { - await operationController.index(inputArrayMock as InputArray); - } catch (e) { - expect(e).toBeInstanceOf(HttpException); - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts deleted file mode 100644 index 4ce1510b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { - Body, - Controller, - Inject, - MethodNotAllowedException, - NotFoundException, - Post, - Type, -} from '@nestjs/common'; -import { Module } from '@nestjs/core/injector/module'; - -import { InputArray } from '../utils'; -import { InputOperationPipe } from '../pipes/input-operation.pipe'; -import { ExecuteService, ExplorerService } from '../service'; -import { KEY_MAIN_INPUT_SCHEMA, KEY_MAIN_OUTPUT_SCHEMA } from '../constants'; -import { OperationMethode, ParamsForExecute } from '../types'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; -import { Entity, ValidateQueryError } from '../../../types'; - -@Controller('/') -export class OperationController { - @Inject(ExplorerService) private readonly explorerService!: ExplorerService; - @Inject(ExecuteService) private readonly executeService!: ExecuteService; - - @Post('') - async index(@Body(InputOperationPipe) inputOperationData: InputArray) { - const paramForCall: ParamsForExecute[] = []; - let i = 0; - for (const dataInput of inputOperationData) { - const { - ref: { relationship, id, type }, - op, - } = dataInput; - - let controller: Type>; - let methodName: OperationMethode; - let module: Module; - try { - controller = this.explorerService.getControllerByEntityName(type); - } catch (e) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${type}' does not exist`, - path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'ref', 'type'], - }; - throw new NotFoundException([error]); - } - try { - methodName = this.explorerService.getMethodNameByParam( - op, - id, - relationship - ); - } catch (e) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Operation '${op}' not allowed`, - path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'op'], - }; - throw new MethodNotAllowedException([error]); - } - - const params = this.explorerService.getParamsForMethod( - methodName, - dataInput - ); - - try { - module = this.explorerService.getModulesByController(controller); - } catch (e) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - message: `Resource '${type}' does not exist`, - path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'ref', 'type'], - }; - throw new NotFoundException([error]); - } - - paramForCall.push({ - controller, - methodName, - params, - module, - }); - - i++; - } - const tmpIds: (string | number)[] = []; - for (const item of inputOperationData) { - if (item.op !== 'add') continue; - if (!item.ref.tmpId) continue; - tmpIds.push(item.ref.tmpId); - } - - const result = await this.executeService.run(paramForCall, tmpIds); - - return { - [KEY_MAIN_OUTPUT_SCHEMA]: result.map((i) => ({ - data: i.data, - ...(i.meta ? { meta: i.meta } : {}), - })), - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts deleted file mode 100644 index fd9f15b1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Provider } from '@nestjs/common'; -import { ASYNC_ITERATOR_FACTORY } from '../constants'; - -type ParamsInput = R extends (...arg: infer P) => any ? P : never; - -type ParamsReturn = R extends (...arg: any) => infer P - ? P extends Promise - ? T extends [infer K, ...any] - ? K - : T - : P - : never; - -export type IterateFactory< - R extends (...arg: any) => any = (...arg: any) => any -> = { - createIterator: ( - iterateObject: ParamsInput, - callback: R - ) => { - [Symbol.asyncIterator](): GeneralAsyncIterator< - R, - ParamsInput, - ParamsReturn - >; - }; -}; - -class GeneralAsyncIterator< - R extends (...arg: any[]) => any, - T = ParamsInput, - TReturn = ParamsReturn -> implements AsyncIterator -{ - private counter = 0; - private maxLimit!: number; - - constructor(private iterateObject: T[], private callback: R) { - if (!Array.isArray(iterateObject)) { - throw new Error('Expected iterateObject to be an array'); - } - this.maxLimit = iterateObject.length; - } - - async next(): Promise> { - const items = !Array.isArray(this.iterateObject[this.counter]) - ? [this.iterateObject[this.counter]] - : (this.iterateObject[this.counter] as T[]); - this.counter++; - - if (this.counter <= this.maxLimit) { - return this.callback(...items).then((r: TReturn) => ({ - done: false, - value: r, - })); - } else { - return Promise.resolve({ done: true, value: {} as TReturn }); - } - } -} - -export const AsyncIterate: Provider = { - provide: ASYNC_ITERATOR_FACTORY, - useFactory: () => ({ - createIterator any>( - iterateObject: ParamsInput, - callback: R - ) { - return { - [Symbol.asyncIterator]: () => - new GeneralAsyncIterator(iterateObject, callback), - }; - }, - }), -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts deleted file mode 100644 index 0dded8b9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './zod-input-operation'; -export * from './map-controller-entity'; -export * from './map-entity-name-to-entity'; -export * from './async-iterator'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts deleted file mode 100644 index 64d23a17..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DynamicModule, ValueProvider } from '@nestjs/common'; -import { Type } from '@nestjs/common/interfaces/type.interface'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { MapController } from '../types'; -import { MAP_CONTROLLER_ENTITY } from '../constants'; - -export function MapControllerEntity( - entities: EntityClassOrSchema[], - entityModules: DynamicModule[] -): ValueProvider { - const mapController = entities.reduce((acum, entity, index) => { - const entityModule = entityModules[index]; - if (entityModule.controllers) { - const controller = entityModule.controllers.at(0); - if (controller) acum.set(entity, controller); - } - - return acum; - }, new Map>()); - - return { - provide: MAP_CONTROLLER_ENTITY, - useValue: mapController, - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts deleted file mode 100644 index 8149f47a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { ValueProvider } from '@nestjs/common'; -import { MapEntity } from '../types'; -import { MAP_ENTITY } from '../constants'; -import { camelToKebab, getEntityName } from '../../../helper'; - -export function MapEntityNameToEntity( - entities: EntityClassOrSchema[] -): ValueProvider { - return { - provide: MAP_ENTITY, - useValue: entities.reduce( - (acum, item) => acum.set(camelToKebab(getEntityName(item)), item), - new Map() - ), - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts deleted file mode 100644 index 21d58521..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DataSource } from 'typeorm'; -import { FactoryProvider } from '@nestjs/common'; -import { MAP_CONTROLLER_ENTITY, ZOD_INPUT_OPERATION } from '../constants'; -import { MapController } from '../types'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; -import { - zodInputOperation, - ZodInputOperation as TypeZodInputOperation, -} from '../utils/zod/zod-helper'; - -export function ZodInputOperation( - connectionName?: string -): FactoryProvider { - return { - provide: ZOD_INPUT_OPERATION, - useFactory(dataSource: DataSource, mapController: MapController) { - return zodInputOperation(dataSource, mapController); - }, - inject: [ - { - token: CURRENT_DATA_SOURCE_TOKEN, - optional: false, - }, - { - token: MAP_CONTROLLER_ENTITY, - optional: false, - }, - ], - }; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts deleted file mode 100644 index 643405e1..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ZodError } from 'zod'; -import { - InternalServerErrorException, - BadRequestException, -} from '@nestjs/common'; - -import { InputOperationPipe } from './input-operation.pipe'; - -import { KEY_MAIN_INPUT_SCHEMA, ZOD_INPUT_OPERATION } from '../constants'; -import { ZodInputOperation } from '../utils'; - -describe('PatchInputPipe', () => { - let patchInputPipe: InputOperationPipe; - let zodInputOperation: ZodInputOperation; - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: ZOD_INPUT_OPERATION, - useValue: { - parse() {}, - }, - }, - InputOperationPipe, - ], - }).compile(); - - patchInputPipe = module.get(InputOperationPipe); - zodInputOperation = module.get(ZOD_INPUT_OPERATION); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('It should be ok', () => { - const data = { - some: 'data', - }; - const check = { - [KEY_MAIN_INPUT_SCHEMA]: data, - }; - jest - .spyOn(zodInputOperation, 'parse') - .mockImplementationOnce(() => check as any); - expect(patchInputPipe.transform(check)).toEqual(data); - }); - - it('Should be not ok', () => { - jest.spyOn(zodInputOperation, 'parse').mockImplementationOnce(() => { - throw new ZodError([]); - }); - expect.assertions(1); - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - } - }); - - it('Should be 500', () => { - jest.spyOn(zodInputOperation, 'parse').mockImplementationOnce(() => { - throw new Error('Error mock'); - }); - expect.assertions(1); - - try { - patchInputPipe.transform({}); - } catch (e) { - expect(e).toBeInstanceOf(InternalServerErrorException); - } - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts deleted file mode 100644 index 11ca8652..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { - InternalServerErrorException, - BadRequestException, - Inject, - PipeTransform, -} from '@nestjs/common'; -import { errorMap } from 'zod-validation-error'; -import { ZodError } from 'zod'; -import { JSONValue } from '../../../types'; -import { InputArray, ZodInputOperation } from '../utils'; -import { KEY_MAIN_INPUT_SCHEMA, ZOD_INPUT_OPERATION } from '../constants'; - -export class InputOperationPipe - implements PipeTransform -{ - @Inject(ZOD_INPUT_OPERATION) private zodInputOperation!: ZodInputOperation; - - transform(value: JSONValue): InputArray { - try { - return this.zodInputOperation.parse(value, { - errorMap: errorMap, - })[KEY_MAIN_INPUT_SCHEMA]; - } catch (e) { - if (e instanceof ZodError) { - throw new BadRequestException(e.issues); - } - - throw new InternalServerErrorException(e); - } - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts deleted file mode 100644 index 5b2f6bb7..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts +++ /dev/null @@ -1,478 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { ModuleRef } from '@nestjs/core'; -import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; -import { DataSource } from 'typeorm'; -import { ExecuteService, isZodError } from './execute.service'; -import { IterateFactory } from '../factory'; -import { - ASYNC_ITERATOR_FACTORY, - KEY_MAIN_INPUT_SCHEMA, - MAP_CONTROLLER_INTERCEPTORS, - OPTIONS, -} from '../constants'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; -import { - HttpException, - NotFoundException, - ParseIntPipe, - ParseBoolPipe, -} from '@nestjs/common'; -import { ParamsForExecute } from '../types'; -import { AsyncLocalStorage } from 'async_hooks'; - -describe('ExecuteService', () => { - let service: ExecuteService; - let dataSource: DataSource; - let moduleRef: ModuleRef; - let asyncIteratorFactory: IterateFactory; - let mapControllerInterceptors = new Map(); - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ExecuteService, - { - provide: CURRENT_DATA_SOURCE_TOKEN, - useValue: { - createQueryRunner: () => {}, - }, - }, - { - provide: ModuleRef, - useValue: { - get() {}, - }, - }, - { - provide: OPTIONS, - useValue: {}, - }, - { - provide: ASYNC_ITERATOR_FACTORY, - useValue: { - createIterator: () => {}, - }, - }, - { - provide: MAP_CONTROLLER_INTERCEPTORS, - useValue: mapControllerInterceptors, - }, - { - provide: AsyncLocalStorage, - useValue: new AsyncLocalStorage(), - }, - ], - }).compile(); - - service = module.get(ExecuteService); - dataSource = module.get(CURRENT_DATA_SOURCE_TOKEN); - moduleRef = module.get(ModuleRef); - asyncIteratorFactory = module.get(ASYNC_ITERATOR_FACTORY); - mapControllerInterceptors.clear(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('run', () => { - it('should throw NotFoundException if controller not found', async () => { - const params = [ - { - controller: { name: 'NonExistentController' }, - module: { controllers: new Map() }, - }, - ] as ParamsForExecute[]; - - const queryRunnerMock = { - startTransaction: jest.fn(), - commitTransaction: jest.fn(), - rollbackTransaction: jest.fn(), - release: jest.fn(), - }; - jest - .spyOn(dataSource, 'createQueryRunner') - .mockReturnValue(queryRunnerMock as any); - - jest.spyOn(service as any, 'executeOperations').mockImplementation(() => { - throw new NotFoundException(); - }); - - await expect(service.run(params, [])).rejects.toThrow(NotFoundException); - - expect(queryRunnerMock.rollbackTransaction).toHaveBeenCalled(); - expect(queryRunnerMock.release).toHaveBeenCalled(); - await expect(service.run(params, [])).rejects.toThrow(NotFoundException); - }); - - it('should return an empty array if no operations are executed', async () => { - const params: ParamsForExecute[] = []; - - const queryRunnerMock = { - startTransaction: jest.fn(), - commitTransaction: jest.fn(), - rollbackTransaction: jest.fn(), - release: jest.fn(), - }; - jest - .spyOn(dataSource, 'createQueryRunner') - .mockReturnValue(queryRunnerMock as any); - - jest.spyOn(service as any, 'executeOperations').mockReturnValue([]); - - const result = await service.run(params, []); - expect(result).toEqual([]); - - expect(queryRunnerMock.startTransaction).toHaveBeenCalled(); - expect(queryRunnerMock.commitTransaction).toHaveBeenCalled(); - expect(queryRunnerMock.rollbackTransaction).not.toHaveBeenCalled(); - expect(queryRunnerMock.release).toHaveBeenCalled(); - }); - }); - - describe('executeOperations', () => { - it('should correctly execute operations', async () => { - const params: ParamsForExecute[] = [ - { - controller: { name: 'TestController' }, - methodName: 'someMethod', - }, - ] as unknown as ParamsForExecute[]; - const callback = jest.fn().mockReturnValue({ value: 'test' }); - const mapController = { - someMethod: callback, - }; - jest - .spyOn(service as any, 'getControllerInstance') - .mockReturnValue(mapController); - - mapControllerInterceptors.set(mapController, new Map([[callback, []]])); - let callCount = 0; - jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ - [Symbol.asyncIterator]: () => - ({ - next: () => { - callCount++; - if (callCount === 1) { - return Promise.resolve({ value: 'test', done: false }); - } else { - return Promise.resolve({ value: undefined, done: true }); - } - }, - } as any), - }); - - const result = await (service as any).executeOperations(params, []); - - expect(result).toEqual([{ value: 'test' }]); - }); - - it('should return an empty array if controller method does not return an object', async () => { - const params: ParamsForExecute[] = [ - { - controller: { name: 'TestController' }, - methodName: 'someMethod', - }, - ] as unknown as ParamsForExecute[]; - - const callback = jest.fn().mockReturnValue('not an object'); - const mapController = { - someMethod: callback, - }; - jest - .spyOn(service as any, 'getControllerInstance') - .mockReturnValue(mapController); - - mapControllerInterceptors.set(mapController, new Map([[callback, []]])); - - let callCount = 0; - jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ - [Symbol.asyncIterator]: () => - ({ - next: () => { - callCount++; - if (callCount === 1) { - return Promise.resolve({ value: 'not an object', done: false }); - } else { - return Promise.resolve({ value: undefined, done: true }); - } - }, - } as any), - }); - - const result = await (service as any).executeOperations(params, []); - - expect(result).toEqual([]); - }); - - it('should call processException if an exception is thrown during execution', async () => { - const params: ParamsForExecute[] = [ - { - controller: { name: 'TestController' }, - methodName: 'someMethod', - }, - ] as unknown as ParamsForExecute[]; - - const callback = jest.fn().mockImplementation(() => { - throw new HttpException('Test exception', 400); - }); - const mapController = { - someMethod: callback, - }; - jest - .spyOn(service as any, 'getControllerInstance') - .mockReturnValue(mapController); - - mapControllerInterceptors.set(mapController, new Map([[callback, []]])); - - let callCount = 0; - jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ - [Symbol.asyncIterator]: () => - ({ - next: () => { - callCount++; - if (callCount === 1) { - return Promise.resolve({ value: 'test', done: false }); - } else { - return Promise.resolve({ value: undefined, done: true }); - } - }, - } as any), - }); - - const processExceptionSpy = jest.spyOn( - service as any, - 'processException' - ); - - await expect((service as any).executeOperations(params)).rejects.toThrow( - HttpException - ); - - expect(processExceptionSpy).toHaveBeenCalled(); - }); - }); - - describe('getControllerInstance', () => { - it('should throw NotFoundException if controller not found', () => { - const params: ParamsForExecute = { - controller: { name: 'NonExistentController' }, - module: { controllers: new Map() }, - } as unknown as ParamsForExecute; - - expect(() => (service as any).getControllerInstance(params)).toThrow( - NotFoundException - ); - }); - - it('should return controller instance if controller is found', () => { - const controllerInstance = { - someMethod: jest.fn().mockReturnValue('test'), - }; - function TestController() {} - const params: ParamsForExecute = { - controller: TestController, - methodName: 'someMethod', - module: { - controllers: new Map([ - [TestController, { instance: controllerInstance }], - ]), - }, - } as unknown as ParamsForExecute; - - const result = (service as any).getControllerInstance(params); - - expect(result).toBe(controllerInstance); - }); - }); - - describe('processException', () => { - it('should rethrow HttpException with modified response if ZodError is thrown', () => { - const exception = new HttpException( - { - message: [{ path: ['test'] }], - }, - 400 - ); - - try { - (service as any).processException(exception, 1); - } catch (e) { - if (e instanceof HttpException) { - const response = e.getResponse(); - if (isZodError(response)) { - expect(response['message'][0]['path']).toEqual([ - KEY_MAIN_INPUT_SCHEMA, - '1', - 'test', - ]); - } else { - fail('Exception response is not a ZodError'); - } - } else { - fail('Caught exception is not a HttpException'); - } - } - }); - - it('should rethrow the original exception if it is not a HttpException', () => { - const exception = new Error('Test exception'); - - expect(() => (service as any).processException(exception, 1)).toThrow( - Error - ); - }); - }); - - describe('runOneOperation', () => { - it('should correctly run operation', async () => { - const controllerInstance = { - someMethod: jest.fn().mockReturnValue('test'), - }; - function TestController() {} - const pipes = [ - { index: 0, pipes: [] }, - { index: 1, pipes: [] }, - ]; - const params: ParamsForExecute = { - controller: TestController, - methodName: 'someMethod', - module: { - controllers: new Map([ - [TestController, { instance: controllerInstance }], - ]), - }, - params: ['param1', 'param2'], - } as unknown as ParamsForExecute; - - Reflect.defineMetadata( - ROUTE_ARGS_METADATA, - { 0: pipes[0], 1: pipes[1] }, - TestController, - 'someMethod' - ); - - const runPipesSpy = jest - .spyOn(service as any, 'runPipes') - .mockImplementation((param) => `modified_${param}`); - - await (service as any).runOneOperation(params); - - expect(runPipesSpy).toHaveBeenCalledWith( - 'param1', - params.module, - pipes[0].pipes - ); - expect(runPipesSpy).toHaveBeenCalledWith( - 'param2', - params.module, - pipes[1].pipes - ); - }); - - it('should not call runPipes if metadata is empty', async () => { - const controllerInstance = { - someMethod: jest.fn().mockReturnValue('test'), - }; - function TestController() {} - const params: ParamsForExecute = { - controller: TestController, - methodName: 'someMethod', - module: { - controllers: new Map([ - [TestController, { instance: controllerInstance }], - ]), - }, - params: ['param1', 'param2'], - } as unknown as ParamsForExecute; - - Reflect.defineMetadata( - ROUTE_ARGS_METADATA, - {}, - TestController, - 'someMethod' - ); - - const runPipesSpy = jest - .spyOn(service as any, 'runPipes') - .mockImplementation((param) => `modified_${param}`); - - await (service as any).runOneOperation(params); - - expect(runPipesSpy).not.toHaveBeenCalled(); - }); - }); - - describe('runPipes', () => { - it('should correctly run pipes', async () => { - const value = 'test'; - const pipes = [new ParseBoolPipe(), new ParseIntPipe()]; - const module = {} as any; - - jest - .spyOn(pipes[0], 'transform') - // @ts-ignore - .mockImplementation((val) => `validated_${val}`); - - jest - .spyOn(pipes[1], 'transform') - // @ts-ignore - .mockImplementation((val) => `parsed_${val}`); - const getPipeInstanceSpy = jest - .spyOn(service as any, 'getPipeInstance') - .mockImplementation((pipe) => - pipe instanceof ParseBoolPipe ? pipes[0] : pipes[1] - ); - - const result = await (service as any).runPipes(value, module, [ - pipes[0], - pipes[1], - ]); - - expect(result).toBe('parsed_validated_test'); - expect(getPipeInstanceSpy).toHaveBeenCalledTimes(2); - expect(getPipeInstanceSpy).toHaveBeenNthCalledWith(1, pipes[0], module); - expect(getPipeInstanceSpy).toHaveBeenNthCalledWith(2, pipes[1], module); - }); - - it('should not call getPipeInstance if pipes array is empty', async () => { - const value = 'test'; - const module = {} as any; - - const getPipeInstanceSpy = jest.spyOn(service as any, 'getPipeInstance'); - - const result = await (service as any).runPipes(value, module, []); - - expect(result).toBe('test'); - expect(getPipeInstanceSpy).not.toHaveBeenCalled(); - }); - }); - - describe('getPipeInstance', () => { - it('should return pipe instance from module if it exists', () => { - const pipe = new ParseBoolPipe(); - const module = { - getProviderByKey: jest.fn().mockReturnValue({ instance: pipe }), - } as any; - - const result = (service as any).getPipeInstance(ParseBoolPipe, module); - - expect(result).toBe(pipe); - expect(module.getProviderByKey).toHaveBeenCalledWith(ParseBoolPipe); - }); - - it('should return pipe instance from moduleRef if it does not exist in module', () => { - const pipe = new ParseBoolPipe(); - const module = { - getProviderByKey: jest.fn().mockReturnValue(null), - } as any; - jest.spyOn(service['moduleRef'], 'get').mockReturnValue(pipe); - - const result = (service as any).getPipeInstance(ParseBoolPipe, module); - - expect(result).toBe(pipe); - expect(service['moduleRef'].get).toHaveBeenCalledWith(ParseBoolPipe, { - strict: false, - }); - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts deleted file mode 100644 index 8bec403f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts +++ /dev/null @@ -1,345 +0,0 @@ -import { - HttpException, - NotFoundException, - Inject, - Injectable, - PipeTransform, - Type, -} from '@nestjs/common'; -import { - INTERCEPTORS_METADATA, - ROUTE_ARGS_METADATA, -} from '@nestjs/common/constants'; -import { Module } from '@nestjs/core/injector/module'; -import { ArgumentMetadata } from '@nestjs/common/interfaces/features/pipe-transform.interface'; -import { ApplicationConfig, ModuleRef, NestContainer } from '@nestjs/core'; -import { DataSource } from 'typeorm'; - -import { MapControllerInterceptor, ParamsForExecute } from '../types'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; -import { - ASYNC_ITERATOR_FACTORY, - KEY_MAIN_INPUT_SCHEMA, - MAP_CONTROLLER_INTERCEPTORS, - OPTIONS, -} from '../constants'; -import { IterateFactory } from '../factory'; -import { - ConfigParam, - ResourceObject, - ResourceObjectRelationships, - TypeFromType, - ValidateQueryError, -} from '../../../types'; -import { ObjectTyped } from '../../../helper'; -import { - InterceptorsConsumer, - InterceptorsContextCreator, -} from '@nestjs/core/interceptors'; -import { Controller } from '@nestjs/common/interfaces'; -import { lastValueFrom } from 'rxjs'; -import { AsyncLocalStorage } from 'async_hooks'; - -export function isZodError( - param: string | unknown -): param is { message: ValidateQueryError[] } { - return ( - param instanceof Object && - 'message' in param && - Array.isArray(param.message) && - 'path' in param.message[0] - ); -} - -@Injectable() -export class ExecuteService { - @Inject(CURRENT_DATA_SOURCE_TOKEN) private readonly dataSource!: DataSource; - @Inject(ModuleRef) private readonly moduleRef!: ModuleRef & { - container: NestContainer; - applicationConfig: ApplicationConfig; - _moduleKey: string; - }; - @Inject(ASYNC_ITERATOR_FACTORY) private asyncIteratorFactory!: IterateFactory< - ExecuteService['runOneOperation'] - >; - @Inject(OPTIONS) private options!: ConfigParam; - @Inject(MAP_CONTROLLER_INTERCEPTORS) - private mapControllerInterceptor!: MapControllerInterceptor; - - @Inject(AsyncLocalStorage) private asyncLocalStorage!: AsyncLocalStorage; - - private _interceptorsContextCreator!: InterceptorsContextCreator; - - get interceptorsContextCreator() { - if (!this._interceptorsContextCreator) { - this._interceptorsContextCreator = new InterceptorsContextCreator( - this.moduleRef.container, - this.moduleRef.applicationConfig - ); - } - - return this._interceptorsContextCreator; - } - - private interceptorsConsumer = new InterceptorsConsumer(); - - async run(params: ParamsForExecute[], tmpIds: (string | number)[]) { - if ( - this.options.runInTransaction && - typeof this.options.runInTransaction === 'function' - ) { - return this.options.runInTransaction('READ UNCOMMITTED', () => { - return this.executeOperations(params, tmpIds); - }); - } - const queryRunner = this.dataSource.createQueryRunner(); - await queryRunner.startTransaction('READ UNCOMMITTED'); - try { - const resultArray = await this.executeOperations(params, tmpIds); - await queryRunner.commitTransaction(); - return resultArray; - } catch (e) { - await queryRunner.rollbackTransaction(); - throw e; - } finally { - await queryRunner.release(); - } - - return []; - } - - private async executeOperations( - params: ParamsForExecute[], - tmpIds: (string | number)[] = [] - ) { - const iterateParams = this.asyncIteratorFactory.createIterator( - params as Parameters, - this.runOneOperation.bind(this) as ExecuteService['runOneOperation'] - ); - - const resultArray: Array< - ResourceObject | ResourceObjectRelationships - > = []; - let i = 0; - const tmpIdsMap: Record = {}; - try { - for await (const item of iterateParams) { - const currentParams = params[i]; - const controller = this.getControllerInstance(currentParams); - const methodName = - currentParams.methodName as (typeof currentParams)['methodName']; - - const paramsForExecute = item as unknown as ParamsForExecute['params']; - - const itemReplace = this.replaceTmpIds(paramsForExecute, tmpIdsMap); - const body = itemReplace.at(-1); - const currentTmpId = tmpIds[i]; - - if (methodName === 'postOne' && currentTmpId && body) { - if (typeof body === 'object' && 'attributes' in body) { - body['id'] = `${currentTmpId}`; - itemReplace[itemReplace.length - 1]; - } - } - - const interceptors = this.getInterceptorsArray( - controller, - controller[methodName], - currentParams.module - ); - - const result$: any = await this.interceptorsConsumer.intercept( - interceptors, - [ - ...Object.values(this.asyncLocalStorage.getStore() || {}), - itemReplace, - ], - controller, - // @ts-ignore - controller[methodName], - // @ts-ignore - async () => controller[methodName](...itemReplace) - ); - - const result = - interceptors.length === 0 - ? await result$ - : await lastValueFrom(result$); - - if (tmpIds[i] && result && !Array.isArray(result.data) && result.data) { - tmpIdsMap[tmpIds[i]] = result.data.id; - } - - if (result instanceof Object) { - resultArray.push(result); - } - i++; - } - } catch (e) { - this.processException(e, i); - } - return resultArray; - } - - private getInterceptorsArray( - controller: Controller, - callback: (...arg: any) => any, - module: ParamsForExecute['module'] - ) { - let controllerFromMap = this.mapControllerInterceptor.get(controller); - - if (!controllerFromMap) { - controllerFromMap = new Map(); - this.mapControllerInterceptor.set(controller, controllerFromMap); - } - - const interceptorsFromMap = controllerFromMap.get(callback); - - if (interceptorsFromMap) { - return interceptorsFromMap; - } - - const interceptorsForController = this.interceptorsContextCreator.create( - controller, - callback, - module.token - ); - - const interceptorsForMethode = new Set( - Reflect.getMetadata(INTERCEPTORS_METADATA, callback) || [] - ); - - const resultInterceptors = interceptorsForController.filter((i) => - interceptorsForMethode.has(i.constructor) - ); - controllerFromMap.set(callback, resultInterceptors); - return resultInterceptors; - } - - private replaceTmpIds( - inputParams: T, - tmpIdsMap: Record - ): T { - const bodyInput = inputParams.at(-1); - if (!bodyInput) { - return inputParams; - } - if (typeof bodyInput === 'string') { - return inputParams; - } - if (typeof bodyInput === 'number') { - return inputParams; - } - - if (Array.isArray(bodyInput)) { - return inputParams; - } - - if (!('relationships' in bodyInput)) { - return inputParams; - } - - const { relationships } = bodyInput; - if (!relationships) { - return inputParams; - } - - bodyInput.relationships = ObjectTyped.entries(relationships).reduce( - (acum, [name, val]) => { - if (Array.isArray(val)) { - acum[name] = (val as any[]).map((i) => { - i['id'] = tmpIdsMap[i['id']] ? tmpIdsMap[i['id']] : i['id']; - return i; - }) as never; - } else { - acum[name]['id'] = tmpIdsMap[val['id']] - ? (tmpIdsMap[val['id']] as never) - : acum[name]['id']; - } - return acum; - }, - // @ts-ignore - { ...relationships } - ); - - inputParams[inputParams.length - 1] = bodyInput; - return inputParams; - } - - private getControllerInstance(params: ParamsForExecute) { - const controllerClass = params.controller; - const controllerInstanceWrapper = - params.module.controllers.get(controllerClass); - - if (!controllerInstanceWrapper) { - const error: ValidateQueryError = { - code: 'invalid_arguments', - path: ['type'], - message: `Controller "${controllerClass.name}" not found`, - }; - throw new NotFoundException([error]); - } - - return controllerInstanceWrapper.instance as TypeFromType< - ParamsForExecute['controller'] - >; - } - - private processException(e: any, i: number) { - if (e instanceof HttpException) { - const response = e.getResponse(); - if (isZodError(response)) { - response['message'] = response['message'].map((m: any) => { - m['path'] = [KEY_MAIN_INPUT_SCHEMA, `${i}`, ...m['path']]; - return m; - }); - } - throw new HttpException(response, e.getStatus()); - } - throw e; - } - - private async runOneOperation( - paramForExecute: ParamsForExecute - ): Promise { - const { params, controller, methodName, module } = paramForExecute; - const pramsPipe = Object.values( - Reflect.getMetadata(ROUTE_ARGS_METADATA, controller, methodName) - ) as unknown as { - index: number; - pipes: Type[]; - }[]; - const resultParams = new Array(params.length); - for (const { pipes, index } of pramsPipe) { - resultParams[index] = await this.runPipes(params[index], module, pipes); - } - return resultParams as unknown as ParamsForExecute['params']; - } - - private async runPipes( - initialParams: unknown, - module: Module, - pipes: Type[] - ) { - let modifiedParams = initialParams; - for (const pipe of pipes) { - const pipeInstance = this.getPipeInstance(pipe, module); - modifiedParams = await pipeInstance.transform( - modifiedParams, - {} as ArgumentMetadata - ); - } - return modifiedParams; - } - - private getPipeInstance( - pipe: Type, - module: Module - ): PipeTransform { - const instanceWrapper = module.getProviderByKey(pipe); - if (!instanceWrapper) { - return this.moduleRef.get(pipe, { strict: false }); - } - return instanceWrapper.instance; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts deleted file mode 100644 index ec654d2d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Test } from '@nestjs/testing'; -import { ModulesContainer } from '@nestjs/core'; -import { - MAP_ENTITY, - MAP_CONTROLLER_ENTITY, - OPTIONS, - MAP_CONTROLLER_INTERCEPTORS, -} from '../constants'; -import { Operation } from '../utils'; -import { ExplorerService } from './explorer.service'; - -describe('ExplorerService', () => { - let service: ExplorerService; - class EntityName {} - class ControllerName {} - beforeEach(async () => { - const moduleRef = await Test.createTestingModule({ - providers: [ - ExplorerService, - { - provide: ModulesContainer, - useValue: new Map([ - [ - 'TestModule', - { - controllers: new Map([['ControllerName', ControllerName]]), - }, - ], - ]), - }, - { - provide: MAP_ENTITY, - useValue: new Map([['EntityName', EntityName]]), - }, - { - provide: MAP_CONTROLLER_ENTITY, - useValue: new Map([[EntityName, ControllerName]]), - }, - { - provide: MAP_CONTROLLER_INTERCEPTORS, - useValue: new Map(), - }, - { - provide: OPTIONS, - useValue: {}, - }, - ], - }).compile(); - - service = moduleRef.get(ExplorerService); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('getControllerByEntityName()', () => { - it('should return the correct controller for a given entity name', () => { - expect(service.getControllerByEntityName('EntityName')).toBeDefined(); - }); - }); - - describe('getMethodNameByParam()', () => { - it('should return the correct method name for given parameters', () => { - expect(service.getMethodNameByParam(Operation.add, 'id', 'rel')).toBe( - 'postRelationship' - ); - }); - }); - - describe('getParamsForMethod()', () => { - it('should return the correct parameters for a given method name', () => { - const data = { - ref: { - id: '1', - relationship: 'belongs-to', - type: 'TypeA', - }, - op: Operation.add, - data: {}, - }; - expect(service.getParamsForMethod('patchRelationship', data)).toEqual([ - data.ref.id, - data.ref.relationship, - { data: data.data }, - ]); - }); - }); - - describe('getModulesByController()', () => { - it('should return the correct module for a given controller', () => { - expect( - service.getModulesByController(ControllerName as any) - ).toBeDefined(); - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts deleted file mode 100644 index 582421ef..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Inject, Injectable, Type } from '@nestjs/common'; -import { Module } from '@nestjs/core/injector/module'; -import { ModulesContainer } from '@nestjs/core'; -import { MAP_CONTROLLER_ENTITY, MAP_ENTITY } from '../constants'; -import { MapController, MapEntity, OperationMethode } from '../types'; -import { Entity, EntityRelation } from '../../../types'; -import { InputArray, Operation } from '../utils'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; -import { - PatchData, - PatchRelationshipData, - PostData, - PostRelationshipData, -} from '../../../helper/zod'; - -@Injectable() -export class ExplorerService { - @Inject(ModulesContainer) - private readonly modulesContainer!: ModulesContainer; - - @Inject(MAP_ENTITY) private readonly mapEntity!: MapEntity; - @Inject(MAP_CONTROLLER_ENTITY) private readonly mapController!: MapController; - - private mapModuleByController = new Map< - Type>, - Module - >(); - - getControllerByEntityName(entityName: string): Type> { - const entity = this.mapEntity.get(entityName); - if (!entity) { - throw new Error(); - } - - const controller = this.mapController.get(entity); - if (!controller) { - throw new Error(); - } - - return controller; - } - - getMethodNameByParam( - operation: Operation, - id?: string, - rel?: string - ): OperationMethode { - switch (operation) { - case Operation.add: - return id ? 'postRelationship' : 'postOne'; - case Operation.remove: - return rel ? 'deleteRelationship' : 'deleteOne'; - case Operation.update: - return rel ? 'patchRelationship' : 'patchOne'; - default: - throw new Error(); - } - } - - getParamsForMethod( - methodName: OperationMethode, - data: InputArray[number] - ): Parameters[typeof methodName]> { - const { op, ref, ...other } = data; - switch (methodName) { - case 'postOne': - return [other as PostData]; - case 'patchOne': - return [ref.id as string, other as PatchData]; - case 'deleteOne': - return [ref.id as string]; - case 'deleteRelationship': - return [ - ref.id as string, - ref.relationship as EntityRelation, - other as PostRelationshipData, - ]; - case 'patchRelationship': - return [ - ref.id as string, - ref.relationship as EntityRelation, - other as PatchRelationshipData, - ]; - case 'postRelationship': - return [ - ref.id as string, - ref.relationship as EntityRelation, - other as PostRelationshipData, - ]; - } - } - - getModulesByController(controllers: Type>): Module { - const module = this.mapModuleByController.get(controllers); - if (module) { - return module; - } - - const findModule = [...this.modulesContainer.values()].find((i) => - [...i.controllers.values()].find((c) => c.name === controllers.name) - ); - if (findModule) { - return findModule; - } - - throw new Error(); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts deleted file mode 100644 index aaf75a95..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './explorer.service'; -export * from './execute.service'; -export * from './swagger.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts deleted file mode 100644 index cf263440..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; -import { ModuleRef } from '@nestjs/core'; -import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { generateSchema } from '@anatine/zod-openapi'; -import { - ReferenceObject, - SchemaObject, -} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; - -import { OperationController } from '../controllers'; -import { ZodInputOperation } from '../utils'; -import { ZOD_INPUT_OPERATION } from '../constants'; - -@Injectable() -export class SwaggerService implements OnModuleInit { - @Inject(ModuleRef) private readonly moduleRef!: ModuleRef; - @Inject(ZOD_INPUT_OPERATION) - private typeZodInputOperation!: ZodInputOperation; - - private initSwagger() { - const operationControllerInst = this.moduleRef.get(OperationController); - if (!operationControllerInst) - throw new Error('OperationController not found'); - const controller = operationControllerInst.constructor.prototype; - const descriptor = Reflect.getOwnPropertyDescriptor(controller, 'index'); - if (!descriptor) - throw new Error(`Descriptor for controller OperationController is empty`); - - ApiTags('Atomic operation')(operationControllerInst.constructor); - ApiOperation({ - summary: `Atomic operation for several entity"`, - operationId: `atomic_operation`, - })(controller, 'index', descriptor); - - ApiBody({ - description: `Json api schema for new atomic operatiom`, - schema: generateSchema(this.typeZodInputOperation) as - | SchemaObject - | ReferenceObject, - required: true, - examples: { - allField: { - summary: 'Examples several operation', - description: 'Examples several operation', - value: { - ['atomic:operations']: [ - { - op: 'add', - ref: { - type: 'users', - }, - data: 'EntityPostOne', - }, - { - op: 'update', - ref: { - type: 'users', - id: '1', - }, - data: 'EntityPatchOne', - }, - { - op: 'remove', - ref: { - type: 'users', - id: '1', - }, - }, - { - op: 'add', - ref: { - type: 'users', - id: '1', - relationship: 'EntityRelationName', - }, - data: 'UsersPostRelationship', - }, - { - op: 'update', - ref: { - type: 'users', - id: '1', - relationship: 'EntityRelationName', - }, - data: 'UsersDeleteRelationship', - }, - ], - }, - }, - }, - })(controller, 'index', descriptor); - } - - onModuleInit(): void { - this.initSwagger(); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts deleted file mode 100644 index f01175a9..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NestInterceptor, Type } from '@nestjs/common'; -import { Module } from '@nestjs/core/injector/module'; -import { Controller } from '@nestjs/common/interfaces'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { Entity, MethodName } from '../../../types'; -import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; - -export type MapControllerInterceptor = Map< - Controller, - Map<(...arg: any) => any, NestInterceptor[]> ->; -export type MapController = Map>; -export type MapEntity = Map; - -export type OperationMethode = keyof Omit< - { [k in MethodName]: string }, - 'getAll' | 'getOne' | 'getRelationship' ->; - -export type ParamsForExecute< - E extends Entity = Entity, - O extends OperationMethode = OperationMethode -> = { - methodName: O; - controller: Type>; - params: Parameters[O]>; - module: Module; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts deleted file mode 100644 index 405fa85d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './zod/zod-helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts deleted file mode 100644 index 49d36c01..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts +++ /dev/null @@ -1,575 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { DataSource } from 'typeorm'; -import { IMemoryDb } from 'pg-mem'; -import { z, ZodError } from 'zod'; -import { - Operation, - ZodAdd, - zodAdd, - zodInputOperation, - ZodInputOperation, - zodOperationRel, - ZodOperationRel, - zodRemove, - ZodRemove, - zodUpdate, - ZodUpdate, -} from './zod-helper'; -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - Users, -} from '../../../../mock-utils'; -import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; -import { JsonBaseController } from '../../../../mixin/controller/json-base.controller'; -import { MapController } from '../../types'; -import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; - -describe('ZodHelperSpec', () => { - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - describe('zodAdd', () => { - it('should be correct', () => { - const user = 'user'; - const schema = zodAdd(user); - const check: z.infer> = { - op: Operation.add, - ref: { - type: user, - }, - data: {}, - }; - const check1: z.infer> = { - op: Operation.add, - ref: { - type: user, - }, - data: [{}], - }; - const check2: z.infer> = { - op: Operation.add, - ref: { - type: user, - }, - data: null, - }; - const check3: z.infer> = { - op: Operation.add, - ref: { - type: user, - }, - data: null, - }; - const checkArray = [check, check1, check2, check3]; - for (const item of checkArray) { - const result = schema.parse(item); - expect(result.op).toBe(Operation.add); - expect(result.ref.type).toBe(user); - expect(result).toHaveProperty('data'); - } - }); - it('should be not correct', () => { - const schema = zodAdd('user'); - const check = { - op: Operation.add, - ref: { - type: 'user', - }, - data: {}, - sdfsf: {}, - }; - const check1 = { - op: Operation.add, - ref: { - type: 'user', - }, - }; - const check2 = { - op: Operation.add, - ref: { - type: 'user', - sdsdf: 'ssdfdsf', - }, - data: {}, - }; - const check3 = { - op: Operation.add, - ref: { - type12: 'user', - }, - data: {}, - }; - const check4 = { - op: Operation.add, - ref: { - type: 'sdfsdf', - }, - data: {}, - }; - const check5 = { - op: 'sdfsdf', - ref: { - type: 'user', - }, - data: {}, - }; - - const checkArray = [check, check1, check2, check3, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('zodUpdate', () => { - it('should be correct', () => { - const user = 'user'; - const schema = zodUpdate(user); - const check: z.infer> = { - op: Operation.update, - ref: { - type: 'user', - id: '1', - }, - data: {}, - }; - const checkArray = [check]; - for (const item of checkArray) { - const result = schema.parse(item); - expect(result.op).toBe(Operation.update); - expect(result.ref.type).toBe(user); - expect(result).toHaveProperty('data'); - } - }); - it('should be not correct', () => { - const schema = zodUpdate('user'); - const check = { - op: Operation.update, - ref: { - type: 'user', - id: '12', - }, - data: {}, - sdfsf: {}, - }; - const check1 = { - op: Operation.update, - ref: { - type: 'user', - id: '12', - }, - }; - const check2 = { - op: Operation.update, - ref: { - type: 'user', - id: '12', - sdsdf: 'ssdfdsf', - }, - data: {}, - }; - const check3 = { - op: Operation.update, - ref: { - type12: 'user', - id: '12', - }, - data: {}, - }; - const check4 = { - op: Operation.update, - ref: { - type: 'sdfsdf', - id: '12', - }, - data: {}, - }; - const check5 = { - op: 'sdfsdf', - ref: { - type: 'user', - id: '12', - }, - data: {}, - }; - const check6 = { - op: Operation.update, - ref: { - type: 'user', - }, - data: {}, - }; - - const checkArray = [ - check, - check1, - check2, - check3, - check4, - check5, - check6, - ]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('zodRemove', () => { - it('should be correct', () => { - const user = 'user'; - const schema = zodRemove(user); - const check: z.infer> = { - op: Operation.remove, - ref: { - type: 'user', - id: '1', - }, - }; - const checkArray = [check]; - for (const item of checkArray) { - const result = schema.parse(item); - expect(result.op).toBe(Operation.remove); - expect(result.ref.type).toBe(user); - expect(result).not.toHaveProperty('data'); - } - }); - - it('should be not correct', () => { - const schema = zodRemove('user'); - const check = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - }, - sdfsf: {}, - }; - const check1 = { - op: Operation.remove, - ref: { - type: 'user', - idsdf: '12', - }, - }; - const check2 = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - sdsdf: 'ssdfdsf', - }, - }; - const check3 = { - op: Operation.remove, - ref: { - type12: 'user', - id: '12', - }, - }; - const check4 = { - op: Operation.remove, - ref: { - type: 'sdfsdf', - id: '12', - }, - }; - const check5 = { - op: 'sdfsdf', - ref: { - type: 'user', - id: '12', - }, - }; - const check6 = { - op: Operation.remove, - ref: { - type: 'user', - }, - }; - - const checkArray = [ - check, - check1, - check2, - check3, - check4, - check5, - check6, - ]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('zodOperationRel', () => { - it('should be correct', () => { - const user = 'user'; - const rel: ['address', 'notes'] = ['address', 'notes']; - const schema = zodOperationRel(user, rel, Operation.remove); - const check: z.infer< - ZodOperationRel<'user', ['address', 'notes'], Operation.remove> - > = { - op: Operation.remove, - ref: { - type: 'user', - id: '1', - relationship: 'notes', - }, - data: { - id: 1, - type: 'notes', - }, - }; - const checkArray = [check]; - for (const item of checkArray) { - const result = schema.parse(item); - expect(result.op).toBe(Operation.remove); - expect(result.ref.type).toBe(user); - expect(result).toHaveProperty('data'); - expect(result['data']).toEqual(check.data); - } - }); - it('should be not correct', () => { - const user = 'user'; - const rel: ['address', 'notes'] = ['address', 'notes']; - const schema = zodOperationRel(user, rel, Operation.remove); - - const check = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - relationship: 'notes', - }, - data: {}, - sdfsf: {}, - }; - const check1 = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - relationship: 'notes', - sdfsdf: 'sdfsdf', - }, - data: {}, - }; - const check2 = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - relationship1: 'notes', - }, - data: {}, - }; - const check3 = { - op: Operation.remove, - ref: { - type12: 'user', - id: '12', - relationship: 'notes', - }, - data: {}, - }; - const check4 = { - op: Operation.remove, - ref: { - type: 'sdfsdf', - id: '12', - relationship: 'notes', - }, - data: {}, - }; - const check5 = { - op: 'sdfsdf', - ref: { - type: 'user', - id: '12', - relationship: 'notes', - }, - data: {}, - }; - const check6 = { - op: Operation.remove, - ref: { - type: 'user', - id: '12', - relationship: 'notes1', - }, - data: {}, - }; - - const checkArray = [ - check, - check1, - check2, - check3, - check4, - check5, - check6, - ]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); - describe('zodInputOperation', () => { - let db: IMemoryDb; - let dataSource: DataSource; - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [...providerEntities(getDataSourceToken())], - }).compile(); - dataSource = module.get( - getDataSourceToken(DEFAULT_CONNECTION_NAME) - ); - }); - - it('should be correct', () => { - const mapController: MapController = new Map([ - [Users as any, JsonBaseController], - ]); - const schema = zodInputOperation(dataSource, mapController); - const check: z.infer = { - [KEY_MAIN_INPUT_SCHEMA]: [ - { - data: {}, - op: Operation.update, - ref: { - type: 'users', - relationship: 'manager', - id: '1', - }, - }, - { - data: {}, - op: Operation.update, - ref: { - type: 'users', - id: '1', - }, - }, - { - data: {}, - op: Operation.add, - ref: { - type: 'users', - }, - }, - { - op: Operation.remove, - ref: { - type: 'users', - id: '1', - }, - }, - ], - }; - expect(schema.parse(check)).toEqual(check); - }); - - it('incorrect input main data', () => { - const mapController: MapController = new Map([ - [Users as any, JsonBaseController], - ]); - const schema = zodInputOperation(dataSource, mapController); - const check = {}; - const check1 = { - ssdf: 'sdfsdf', - }; - const check2 = { - [KEY_MAIN_INPUT_SCHEMA]: null, - }; - const check3 = { - [KEY_MAIN_INPUT_SCHEMA]: '', - }; - const check4 = { - [KEY_MAIN_INPUT_SCHEMA]: {}, - }; - const check5 = { - [KEY_MAIN_INPUT_SCHEMA]: [], - }; - const checkArray = [check, check1, check2, check3, check4, check5]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - - it('should be incorrect methode not allow', () => { - class Test extends JsonBaseController { - override deleteOne(id: string | number): Promise { - return super.deleteOne(id); - } - } - const mapController: MapController = new Map([[Users as any, Test]]); - const schema = zodInputOperation(dataSource, mapController); - const check: z.infer = { - [KEY_MAIN_INPUT_SCHEMA]: [ - { - data: {}, - op: Operation.update, - ref: { - type: 'users', - relationship: 'manager', - id: '1', - }, - }, - ], - }; - const check1: z.infer = { - [KEY_MAIN_INPUT_SCHEMA]: [ - { - data: {}, - op: Operation.remove, - ref: { - type: 'users1', - relationship: 'manager', - id: '1', - }, - }, - ], - }; - const checkArray = [check, check1]; - expect.assertions(checkArray.length); - for (const item of checkArray) { - try { - schema.parse(item); - } catch (e) { - expect(e).toBeInstanceOf(ZodError); - } - } - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts deleted file mode 100644 index 19d6ce68..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { - z, - ZodArray, - ZodLiteral, - ZodNullable, - ZodNumber, - ZodObject, - ZodOptional, - ZodString, - ZodType, - ZodTypeAny, - ZodTypeDef, - ZodUnion, -} from 'zod'; -import { ZodUnionOptions } from 'zod/lib/types'; -import { DataSource } from 'typeorm'; - -import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; -import { MapController } from '../../types'; -import { UnionToTupleMain } from '../../../../types'; -import { EntityTarget } from 'typeorm/common/EntityTarget'; -import { camelToKebab, getEntityName } from '../../../../helper'; -import { getField } from '../../../../helper/orm'; - -export enum Operation { - add = 'add', - update = 'update', - remove = 'remove', -} - -const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); -type Literal = z.infer; -type Json = Literal | { [key: string]: Json } | Json[]; - -const jsonSchema: ZodType = z.lazy(() => - z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) -); - -type ZodGeneral = ZodNullable>; -const zodGeneralData: ZodGeneral = jsonSchema.nullable(); - -export type ZodAdd = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - tmpId: ZodOptional>; - }>; - data: ZodGeneral; -}>; -export const zodAdd = (type: T): ZodAdd => - z - .object({ - op: z.literal(Operation.add), - ref: z - .object({ - type: z.literal(type), - tmpId: z.union([z.number(), z.string()]).optional(), - }) - .strict(), - data: zodGeneralData, - }) - .strict(); - -export type ZodUpdate = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - id: ZodString; - }>; - data: ZodGeneral; -}>; -export const zodUpdate = (type: T): ZodUpdate => - z - .object({ - op: z.literal(Operation.update), - ref: z - .object({ - type: z.literal(type), - id: z.string(), - }) - .strict(), - data: zodGeneralData, - }) - .strict(); -export type ZodRemove = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - id: ZodString; - }>; -}>; -export const zodRemove = (type: T): ZodRemove => - z - .object({ - op: z.literal(Operation.remove), - ref: z - .object({ - type: z.literal(type), - id: z.string(), - }) - .strict(), - }) - .strict(); - -type RelToLiteralArray = UnionToTupleMain< - { - [K in Rel[number]]: ZodLiteral; - }[Rel[number]] ->; - -type RelLiteralArrayToUnion = T extends ZodUnionOptions - ? ZodUnion - : never; - -type ZodRelLiteral = RelLiteralArrayToUnion< - RelToLiteralArray ->; - -export type ZodOperationRel< - T extends string, - Rel extends [string, ...string[]], - OP extends Operation -> = ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodLiteral; - id: ZodString; - relationship: ZodRelLiteral; - }>; - data: ZodGeneral; -}>; -export const zodOperationRel = < - T extends string, - R extends [string, ...string[]], - OP extends Operation ->( - type: T, - rel: R, - typeOperation: OP -): ZodOperationRel => { - const literalArray = rel.map((i) => z.literal(i)) as [ - ZodLiteral, - ZodLiteral, - ...ZodLiteral[] - ]; - - return z - .object({ - op: z.literal(typeOperation), - ref: z - .object({ - type: z.literal(type), - id: z.string(), - relationship: z.union(literalArray) as ZodRelLiteral, - }) - .strict(), - data: zodGeneralData, - }) - .strict(); -}; -export type ZodInputArray = ZodArray< - ZodObject<{ - op: ZodLiteral; - ref: ZodObject<{ - type: ZodString; - id: ZodOptional; - relationship: ZodOptional; - tmpId: ZodOptional>; - }>; - data: ZodOptional; - }>, - 'atleastone' ->; -export type InputArray = z.infer; - -export type ZodInputOperation = ZodObject< - { - [KEY_MAIN_INPUT_SCHEMA]: ZodInputArray; - }, - 'strict' ->; - -export const zodInputOperation = ( - dataSource: DataSource, - mapController: MapController -): ZodInputOperation => { - const array: [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]] = [] as any; - for (const [entity, controller] of mapController.entries()) { - type Entity = typeof entity; - const repository = dataSource.getRepository( - entity as EntityTarget - ); - - const typeName = camelToKebab(getEntityName(repository.target)); - const { relations } = getField(repository); - - const hasOwnProperty = (props: string) => - Object.prototype.hasOwnProperty.call(controller.prototype, props); - - if (hasOwnProperty('postOne')) { - array.push(zodAdd(typeName)); - } - if (hasOwnProperty('patchOne')) { - array.push(zodUpdate(typeName)); - } - if (hasOwnProperty('deleteOne')) { - array.push(zodRemove(typeName)); - } - if (hasOwnProperty('postRelationship')) { - array.push(zodOperationRel(typeName, relations, Operation.add)); - } - if (hasOwnProperty('deleteRelationship')) { - array.push(zodOperationRel(typeName, relations, Operation.remove)); - } - if (hasOwnProperty('patchRelationship')) { - array.push(zodOperationRel(typeName, relations, Operation.update)); - } - } - - return z - .object({ - [KEY_MAIN_INPUT_SCHEMA]: z.array(z.union(array)).nonempty(), - }) - .strict() as unknown as ZodInputOperation; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts deleted file mode 100644 index 60a52e3a..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -import { - createAndPullSchemaBase, - mockDBTestModule, - providerEntities, - UserGroups, - Users, -} from '../mock-utils'; -import { CurrentDataSourceProvider } from '../factory'; -import { DEFAULT_CONNECTION_NAME } from '../constants'; -import { EntityPropsMapService } from './entity-props-map.service'; - -describe('EntityPropsMapService', () => { - let db: IMemoryDb; - let entityPropsMapService: EntityPropsMapService; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityPropsMapService, - ], - }).compile(); - - entityPropsMapService = module.get( - EntityPropsMapService - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('getPropsForEntity', () => { - const result = entityPropsMapService.getRelPropsForEntity(Users); - expect(result).toEqual([ - 'addresses', - 'manager', - 'roles', - 'comments', - 'notes', - 'userGroup', - ]); - }); - - it('getPropsForEntity', () => { - const result = entityPropsMapService.getPropsForEntity(Users); - expect(result).toEqual([ - 'id', - 'login', - 'firstName', - 'testReal', - 'testArrayNull', - 'lastName', - 'isActive', - 'createdAt', - 'testDate', - 'updatedAt', - ]); - }); - - it('getPrimaryColumnsForEntity', () => { - expect(entityPropsMapService.getPrimaryColumnsForEntity(Users)).toBe('id'); - }); - - it('getNameForEntity', () => { - expect(entityPropsMapService.getNameForEntity(Users)).toBe('Users'); - expect(entityPropsMapService.getNameForEntity(UserGroups)).toBe( - 'UserGroups' - ); - }); - - it('getRelationPropsType', () => { - expect( - entityPropsMapService.getRelationPropsType(Users, 'userGroup' as any) - ).toEqual(UserGroups); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts b/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts deleted file mode 100644 index 00263a92..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/entity-props-map.service.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, EntityTarget } from 'typeorm'; - -import { CURRENT_DATA_SOURCE_TOKEN } from '../constants'; -import { Entity, EntityRelation } from '../types'; -import { - getField, - ResultGetField, - TupleOfEntityProps, - TupleOfEntityRelation, -} from '../helper/orm'; - -@Injectable() -export class EntityPropsMapService { - @Inject(CURRENT_DATA_SOURCE_TOKEN) private dataSource!: DataSource; - - private _propsForEntity: Map = new Map(); - private _relPropsForEntity: Map = new Map(); - private _relTypePropsForEntity: Map = new Map(); - private _primaryColumnsForEntity: Map = new Map(); - private _nameForEntity: Map = new Map(); - - getPropsForEntity(entity: E): TupleOfEntityProps { - const result = this._propsForEntity.get(entity); - if (result) { - return result; - } - - const { field } = this.pullPropsAndRelFoEntity(entity); - - return field; - } - - getRelPropsForEntity(entity: E): TupleOfEntityRelation { - const result = this._relPropsForEntity.get(entity); - if (result) { - return result; - } - - const { relations } = this.pullPropsAndRelFoEntity(entity); - - return relations; - } - - getRelationPropsType(entity: E, rel: EntityRelation) { - const result = this._relTypePropsForEntity.get(entity); - if (result) { - return result[rel]; - } - - const repo = this.dataSource.getRepository(entity as EntityTarget); - const relToType = repo.metadata.relations.reduce((acum, item) => { - acum[item.propertyName as keyof E] = item.inverseEntityMetadata.target; - return acum; - }, {} as Record); - - this._relTypePropsForEntity.set(entity, relToType); - return relToType[rel]; - } - - getPrimaryColumnsForEntity(entity: E): keyof E { - const result = this._primaryColumnsForEntity.get(entity); - if (result) { - return result as keyof E; - } - const primaryColumns = this.dataSource.getRepository( - entity as EntityTarget - ).metadata.primaryColumns[0].propertyName; - this._primaryColumnsForEntity.set(entity, primaryColumns); - - return primaryColumns as keyof E; - } - - getNameForEntity(entity: E): string { - const result = this._nameForEntity.get(entity); - if (result) { - return result; - } - const name = this.dataSource.getRepository(entity as EntityTarget) - .metadata.name; - this._nameForEntity.set(entity, name); - return name; - } - - private pullPropsAndRelFoEntity( - entity: E - ): ResultGetField { - const repo = this.dataSource.getRepository(entity as EntityTarget); - - const { relations, field } = getField(repo); - this._propsForEntity.set(entity, field); - this._relPropsForEntity.set(entity, relations); - - return { relations, field }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/service/index.ts deleted file mode 100644 index 952eb04b..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './transform-input.service'; -export * from '../mixin/service/typeorm-utils.service'; -export * from './entity-props-map.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts deleted file mode 100644 index 01fcf175..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.spec.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { TransformInputService } from './transform-input.service'; -import { ASC, DESC } from '../constants'; -import { Users } from '../mock-utils'; -import { QueryField, TypeInputProps } from '../helper'; - -describe('TransformInputService', () => { - let transformInputService: TransformInputService; - - beforeAll(() => { - transformInputService = new TransformInputService(); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('TransformInputService.transformSort', () => { - const check = 'id,manager.id,-roles.id,updatedAt,-createdAt'; - const checkResult = { - target: { - id: ASC, - updatedAt: ASC, - createdAt: DESC, - }, - manager: { - id: ASC, - }, - roles: { - id: DESC, - }, - }; - - const check2 = 'id'; - const checkResult2 = { - target: { - id: ASC, - }, - }; - - const check3 = ''; - - const result = transformInputService.transformSort(check); - const result2 = transformInputService.transformSort(undefined); - const result3 = transformInputService.transformSort(check2); - const result4 = transformInputService.transformSort(check3); - expect(result).toEqual(checkResult); - expect(result2).toBe(null); - expect(result4).toBe(null); - }); - - it('TransformInputService.transformInclude', () => { - const check = 'manager,roles,,notes,'; - const checkResult = ['manager', 'roles', 'notes']; - - const check2 = ''; - - const check3 = 'manager'; - const checkResult3 = ['manager']; - - const result = transformInputService.transformInclude(check); - const result2 = transformInputService.transformInclude(undefined); - const result3 = transformInputService.transformInclude(check2); - const result4 = transformInputService.transformInclude(check3); - expect(result).toEqual(checkResult); - expect(result2).toBe(null); - expect(result3).toBe(null); - expect(result4).toEqual(checkResult3); - }); - - it('TransformInputService.transformFields', () => { - const check = { - target: 'id,updatedAt,createdAt', - manager: 'id', - roles: 'id,key', - }; - const check2 = { - target: 'id,updatedAt,createdAt', - }; - const check3 = { - roles: 'id,key', - }; - const check4 = { - target: '', - roles: '', - }; - const check5 = { - target: 'id,updatedAt,createdAt', - roles: '', - }; - const checkResult = { - target: ['id', 'updatedAt', 'createdAt'], - manager: ['id'], - roles: ['id', 'key'], - }; - const checkResult2 = { - target: ['id', 'updatedAt', 'createdAt'], - }; - const checkResult3 = { - roles: ['id', 'key'], - }; - - const checkResult5 = { - target: ['id', 'updatedAt', 'createdAt'], - }; - - const result = transformInputService.transformFields(check); - const result2 = transformInputService.transformFields(undefined); - const result3 = transformInputService.transformFields({}); - const result4 = transformInputService.transformFields(check2); - const result5 = transformInputService.transformFields(check3 as any); - const result6 = transformInputService.transformFields(check4 as any); - const result7 = transformInputService.transformFields(check5 as any); - expect(result).toEqual(checkResult); - expect(result2).toBe(null); - expect(result3).toBe(null); - expect(result4).toEqual(checkResult2); - expect(result5).toEqual(checkResult3); - expect(result6).toEqual(null); - expect(result7).toEqual(checkResult5); - }); - - it('TransformInputService.transformFilter', () => { - const check1: TypeInputProps = { - id: { - in: 'in-test', - nin: 'nin-test,nin-test2', - }, - isActive: 'true', - manager: { - eq: 'null', - }, - 'addresses.arrayField': { - some: 'some-test', - }, - 'addresses.createdAt': '2023-01-01', - 'userGroup.label': { - like: 'test', - }, - }; - const checkResult1 = { - target: { - id: { - in: ['in-test'], - nin: ['nin-test', 'nin-test2'], - }, - isActive: { - eq: 'true', - }, - manager: { - eq: 'null', - }, - }, - relation: { - addresses: { - arrayField: { - some: ['some-test'], - }, - createdAt: { - eq: '2023-01-01', - }, - }, - userGroup: { - label: { like: 'test' }, - }, - }, - }; - - const check2: TypeInputProps = { - id: '', - 'addresses.arrayField': {}, - 'addresses.createdAt': '2023-01-01', - 'userGroup.label': {}, - }; - const checkResult2 = { - target: { - id: { - eq: '', - }, - }, - relation: { - addresses: { - createdAt: { - eq: '2023-01-01', - }, - }, - }, - }; - - const result1 = transformInputService.transformFilter(check1); - expect(result1).toEqual(checkResult1); - - const result2 = transformInputService.transformFilter(check2); - expect(result2).toEqual(checkResult2); - - expect(transformInputService.transformFilter(undefined)).toEqual({ - target: null, - relation: null, - }); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts b/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts deleted file mode 100644 index c9aadbde..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/service/transform-input.service.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { Entity, FilterOperand } from '../types'; -import { isString } from '../helper/utils'; -import { QueryField, TypeInputProps } from '../helper/zod'; - -import { ASC, DESC } from '../constants'; - -const arrayOp = { - [FilterOperand.in]: true, - [FilterOperand.nin]: true, - [FilterOperand.some]: true, -}; -function convertToFilterObject( - value: Record | string -): Partial<{ - [key in FilterOperand]: string | string[]; -}> { - if (isString | string, string>(value)) { - return { - [FilterOperand.eq]: value, - }; - } else { - return Object.entries(value).reduce((acum, [op, filed]) => { - if (op in arrayOp) { - acum[op] = (isString(filed) ? filed.split(',') : []).filter((i) => !!i); - } else { - acum[op] = filed; - } - return acum; - }, {} as Record); - } -} - -type OutPutFilter = { - relation: null | Record< - string, - Record< - string, - Partial<{ - [key in FilterOperand]: string | string[]; - }> - > - >; - target: null | Record< - string, - Partial<{ - [key in FilterOperand]: string | string[]; - }> - >; -}; - -Injectable(); -export class TransformInputService { - public transformSort( - data: TypeInputProps - ): Record> | null { - if (!data) return null; - return data - .split(',') - .map((i) => i.trim()) - .filter((i) => !!i) - .reduce((acum, field) => { - const fieldName = field.charAt(0) === '-' ? field.substring(1) : field; - const sort = field.charAt(0) === '-' ? DESC : ASC; - if (fieldName.indexOf('.') > -1) { - const [relation, fieldRelation] = field.split('.'); - const relationName = - relation.charAt(0) === '-' ? relation.substring(1) : relation; - - acum[relationName] = acum[relationName] || {}; - acum[relationName][fieldRelation] = sort; - } else { - acum['target'] = acum['target'] || {}; - acum['target'][fieldName] = sort; - } - - return acum; - }, {} as Record>); - } - - public transformInclude( - data: TypeInputProps - ): string[] | null { - if (!data || !isString(data)) return null; - return data - .split(',') - .map((i) => i.trim()) - .filter((i) => !!i); - } - - transformFields( - data: TypeInputProps - ): Record | null { - if (!data) return null; - - const prepareResult = Object.entries(data).reduce((acum, [key, val]) => { - acum[key] = val - .split(',') - .map((i: string) => i.trim()) - .filter((i: string) => !!i); - return acum; - }, {} as Record); - - const result = Object.entries(prepareResult).reduce( - (acum, [key, value]) => { - if (value.length > 0) { - acum[key] = value; - } - return acum; - }, - {} as Record - ); - - return Object.keys(result).length > 0 ? result : null; - } - - transformFilter( - data: TypeInputProps - ): OutPutFilter | null { - if (!data) { - return { - relation: null, - target: null, - }; - } - return Object.entries(data).reduce( - (acum, [field, value]: [string, any]) => { - const objectOperand = convertToFilterObject(value); - if (Object.keys(objectOperand).length === 0) { - return acum; - } - - if (field.indexOf('.') > -1) { - const [relation, fieldRelation] = field.split('.'); - acum['relation'] = !acum['relation'] ? {} : acum['relation']; - acum['relation'][relation] = acum['relation'][relation] || {}; - acum['relation'][relation][fieldRelation] = objectOperand; - } else { - acum['target'] = !acum['target'] ? {} : acum['target']; - acum['target'][field] = objectOperand; - } - - return acum; - }, - { relation: null, target: null } as OutPutFilter - ); - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts deleted file mode 100644 index 21f6d4fb..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/binding.types.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { PipeTransform, RequestMethod } from '@nestjs/common'; -import { Type } from '@nestjs/common/interfaces'; -import { PipeFabric } from './module.types'; -import { JsonBaseController } from '../mixin'; - -export type MethodName = - | 'getAll' - | 'getOne' - | 'getRelationship' - | 'deleteOne' - | 'deleteRelationship' - | 'postOne' - | 'postRelationship' - | 'patchOne' - | 'patchRelationship'; - -type MapNameToTypeMethod = { - getAll: RequestMethod.GET; - getOne: RequestMethod.GET; - patchOne: RequestMethod.PATCH; - patchRelationship: RequestMethod.PATCH; - postOne: RequestMethod.POST; - postRelationship: RequestMethod.POST; - deleteOne: RequestMethod.DELETE; - deleteRelationship: RequestMethod.DELETE; - getRelationship: RequestMethod.GET; -}; - -export interface Binding { - path: string; - method: MapNameToTypeMethod[T]; - name: T; - implementation: JsonBaseController[T]; - parameters: { - decorator: ( - property?: string, - ...pipes: (Type | PipeTransform)[] - ) => ParameterDecorator; - property?: string; - mixins: PipeFabric[]; - }[]; -} - -export type BindingsConfig = { - [Key in MethodName]: Binding; -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts deleted file mode 100644 index 680c34ce..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/decorator-options.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { MethodName } from './binding.types'; -import { ConfigParam } from './module.types'; - -export type DecoratorOptions = Partial< - { - allowMethod: Array; - } & ConfigParam ->; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts deleted file mode 100644 index ca9e3564..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ZodIssue } from 'zod'; - -export type InnerErrorType = - | 'invalid_arguments' - | 'unrecognized_keys' - | 'internal_error'; - -export type InnerError = { - code: InnerErrorType; - message: string; - path: string[]; - keys?: string[]; - error?: Error; -}; - -export type ValidateQueryError = ZodIssue | InnerError; - -export type ErrorDescribe = ValidateQueryError; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/types/index.ts deleted file mode 100644 index 43941180..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './module.types'; -export * from './binding.types'; -export * from './decorator-options.types'; -export * from './utils'; -export * from './operand'; -export * from './error.types'; -export * from './typeorm-service.type'; -export * from './response'; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/module.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/module.types.ts deleted file mode 100644 index 083a56da..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/module.types.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ModuleMetadata, Type, PipeTransform } from '@nestjs/common'; -import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; -import { ObjectLiteral } from 'typeorm/common/ObjectLiteral'; -import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; - -export type NestController = ModuleMetadata['controllers']; -export type NestProvider = ModuleMetadata['providers']; -export type NestImport = ModuleMetadata['imports']; -export type PipeMixin = Type; -export type Entity = EntityClassOrSchema | ObjectLiteral; - -export type PipeFabric = ( - entity: Entity, - connectionName: string, - config?: ConfigParam -) => PipeMixin; - -export type ExtractNestType = - ArrayType extends readonly (infer ElementType)[] ? ElementType : never; - -export interface ConfigParam { - requiredSelectField: boolean; - debug: boolean; - useSoftDelete: boolean; - pipeForId: PipeMixin; - operationUrl?: string; - overrideRoute?: string; - runInTransaction?: any>( - isolationLevel: IsolationLevel, - fn: Func - ) => ReturnType; -} - -export interface ModuleOptions { - entities: EntityClassOrSchema[]; - controllers?: NestController; - connectionName?: string; - providers?: NestProvider; - options?: Partial; - imports?: NestImport; -} - -export interface BaseModuleOptions { - entity: EntityClassOrSchema; - connectionName: string; - controller?: ExtractNestType; - config: ConfigParam; - imports?: NestImport; -} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts b/libs/json-api/json-api-nestjs/src/lib/types/operand.ts deleted file mode 100644 index 10ace773..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { FilterOperand } from 'json-shared-type'; - -export { FilterOperand }; -export const EXPRESSION = 'EXPRESSION'; -export const OperandsMapExpression = { - [FilterOperand.eq]: `= :${EXPRESSION}`, - [FilterOperand.ne]: `<> :${EXPRESSION}`, - [FilterOperand.regexp]: `~* :${EXPRESSION}`, - [FilterOperand.gt]: `> :${EXPRESSION}`, - [FilterOperand.gte]: `>= :${EXPRESSION}`, - [FilterOperand.in]: `IN (:...${EXPRESSION})`, - [FilterOperand.like]: `ILIKE :${EXPRESSION}`, - [FilterOperand.lt]: `< :${EXPRESSION}`, - [FilterOperand.lte]: `<= :${EXPRESSION}`, - [FilterOperand.nin]: `NOT IN (:...${EXPRESSION})`, - [FilterOperand.some]: `&& :${EXPRESSION}`, -}; - -export const OperandMapExpressionForNull = { - [FilterOperand.ne]: 'IS NOT NULL', - [FilterOperand.eq]: 'IS NULL', -}; - -export const OperandsMapExpressionForNullRelation = { - [FilterOperand.ne]: `EXISTS ${EXPRESSION}`, - [FilterOperand.eq]: `NOT EXISTS ${EXPRESSION}`, -}; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/response.ts b/libs/json-api/json-api-nestjs/src/lib/types/response.ts deleted file mode 100644 index 33f67dab..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/response.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { - PageProps, - DebugMetaProps, - MainData, - Links, - Attributes, - Data, - Relationships, - Include, - ResourceData, - ResourceObject, - ResourceObjectRelationships, -} from 'json-shared-type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts b/libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts deleted file mode 100644 index 8062c965..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/typeorm-service.type.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Repository } from 'typeorm'; - -import { ConfigParam, Entity } from './'; -import type { MethodsService } from '../helper'; -import { TypeormUtilsService } from '../service'; -import { TransformDataService } from '../mixin/service'; - -export type TypeormServiceObject = { - repository: Repository; - config: ConfigParam; - typeormUtilsService: TypeormUtilsService; - transformDataService: TransformDataService; -}; - -export type TypeormService = MethodsService; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/utils.ts b/libs/json-api/json-api-nestjs/src/lib/types/utils.ts deleted file mode 100644 index 87021b27..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Type } from '@nestjs/common/interfaces'; -import { EntityField, EntityProps, EntityRelation } from 'json-shared-type'; - -import { Entity } from './module.types'; - -export { EntityField, EntityProps, EntityRelation }; - -export type EntityPropsArray = { - [P in keyof T]: T[P] extends EntityField - ? IsArray extends true - ? P - : never - : never; -}[keyof T]; - -type UnionToIntersection = ( - U extends never ? never : (arg: U) => never -) extends (arg: infer I) => void - ? I - : never; - -export type UnionToTupleMain = UnionToIntersection< - T extends never ? never : (t: T) => T -> extends (_: never) => infer W - ? UnionToTupleMain, [...A, W]> - : A; - -export type UnionToTuple = UnionToTupleMain extends readonly [ - string, - ...string[] -] - ? UnionToTupleMain - : never; - -export type TypeCast = A extends T ? A : never; - -export type TypeOfArray = T extends (infer U)[] ? U : T; - -export type CastProps = K extends keyof E ? E[K] : never; - -export type TypeFromType = T extends Type ? A : never; - -export type ConcatStringArray = T extends [ - infer F extends string, - ...infer R extends string[] -] - ? `${F}${ConcatStringArray}` - : ''; - -export type Concat = ConcatStringArray< - [E, '.', F] ->; - -export type ValueOf = T[keyof T]; - -export type JSONValue = - | string - | number - | boolean - | null - | { [x: string]: JSONValue } - | Array; - -export type IsArray = [Extract] extends [never] ? false : true; diff --git a/libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json b/libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json deleted file mode 100644 index 5857b698..00000000 --- a/libs/json-api/json-api-nestjs/tsconfig-mjs.lib.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "types": ["node"], - "module": "es2015", - "target": "ES2022", - "removeComments": false, - "declaration": true, - "declarationMap": true - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/json-api/json-shared-type/.eslintrc.json b/libs/json-api/json-shared-type/.eslintrc.json deleted file mode 100644 index 3456be9b..00000000 --- a/libs/json-api/json-shared-type/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/libs/json-api/json-shared-type/README.md b/libs/json-api/json-shared-type/README.md deleted file mode 100644 index db1021f7..00000000 --- a/libs/json-api/json-shared-type/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# json-shared-type - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test json-shared-type` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/json-api/json-shared-type/jest.config.ts b/libs/json-api/json-shared-type/jest.config.ts deleted file mode 100644 index 2c9b7818..00000000 --- a/libs/json-api/json-shared-type/jest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'json-shared-type', - preset: '../../../jest.preset.js', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../../coverage/libs/json-api/json-shared-type', -}; diff --git a/libs/json-api/json-shared-type/project.json b/libs/json-api/json-shared-type/project.json deleted file mode 100644 index 6362de6b..00000000 --- a/libs/json-api/json-shared-type/project.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "json-shared-type", - "$schema": "../../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/json-api/json-shared-type/src", - "projectType": "library", - "targets": { - "build1": { - "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/libs/shared-utils", - "tsConfig": "libs/shared-utils/tsconfig.lib.json", - "packageJson": "libs/shared-utils/package.json", - "main": "libs/shared-utils/src/index.ts", - "assets": ["libs/shared-utils/*.md"] - } - } - }, - "tags": [] -} diff --git a/libs/json-api/json-shared-type/src/index.ts b/libs/json-api/json-shared-type/src/index.ts deleted file mode 100644 index fcb073fe..00000000 --- a/libs/json-api/json-shared-type/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './types'; diff --git a/libs/json-api/json-shared-type/src/types/entity-type.ts b/libs/json-api/json-shared-type/src/types/entity-type.ts deleted file mode 100644 index 5fbdd04f..00000000 --- a/libs/json-api/json-shared-type/src/types/entity-type.ts +++ /dev/null @@ -1,17 +0,0 @@ -export type EntityField = - | string - | number - | bigint - | boolean - | string[] - | number[] - | null - | Date; - -export type EntityProps = { - [P in keyof T]: T[P] extends EntityField ? P : never; -}[keyof T]; - -export type EntityRelation = { - [P in keyof T]: T[P] extends EntityField ? never : P; -}[keyof T]; diff --git a/libs/json-api/json-shared-type/src/types/index.ts b/libs/json-api/json-shared-type/src/types/index.ts deleted file mode 100644 index 8b86a958..00000000 --- a/libs/json-api/json-shared-type/src/types/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './entity-type'; -export * from './query-type'; -export * from './utils-type'; -export * from './response-body'; diff --git a/libs/json-api/json-shared-type/src/types/query-type.ts b/libs/json-api/json-shared-type/src/types/query-type.ts deleted file mode 100644 index dba33ee2..00000000 --- a/libs/json-api/json-shared-type/src/types/query-type.ts +++ /dev/null @@ -1,21 +0,0 @@ -export enum QueryField { - filter = 'filter', - sort = 'sort', - include = 'include', - page = 'page', - fields = 'fields', -} - -export enum FilterOperand { - eq = 'eq', - gt = 'gt', - gte = 'gte', - in = 'in', - like = 'like', - lt = 'lt', - lte = 'lte', - ne = 'ne', - nin = 'nin', - regexp = 'regexp', - some = 'some', -} diff --git a/libs/json-api/json-shared-type/src/types/response-body.ts b/libs/json-api/json-shared-type/src/types/response-body.ts deleted file mode 100644 index fbe91dde..00000000 --- a/libs/json-api/json-shared-type/src/types/response-body.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - EntityField, - EntityProps, - EntityRelation, - TypeOfArray, - ValueOf, -} from '.'; - -export type PageProps = { - totalItems: number; - pageNumber: number; - pageSize: number; -}; - -export type DebugMetaProps = Partial<{ - time: number; -}>; - -export type MainData = { - type: T; - id: string; -}; - -export type Links = { - self: string; - related?: string; -}; - -export type Attributes = { - [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; -}; - -export type Data = { - data?: E extends unknown[] ? MainData[] : MainData | null; -}; - -export type Relationships = { - [P in EntityRelation]?: { - links: Links; - } & Data; -}; - -export type Include = ValueOf<{ - [P in EntityRelation]: ResourceData>; -}>; - -export type ResourceData = MainData & { - attributes?: Attributes; - relationships?: Relationships; - links: Omit; -}; - -export type MetaProps = R extends null ? T : T & R; - -export type ResourceObject< - T, - R extends 'object' | 'array' = 'object', - M = null -> = { - meta: R extends 'array' - ? MetaProps - : MetaProps; - data: R extends 'array' ? ResourceData[] : ResourceData; - included?: Include[]; -}; - -export type ResourceObjectRelationships> = { - meta: DebugMetaProps; -} & Required>; diff --git a/libs/json-api/json-shared-type/src/types/utils-type.ts b/libs/json-api/json-shared-type/src/types/utils-type.ts deleted file mode 100644 index ef480a55..00000000 --- a/libs/json-api/json-shared-type/src/types/utils-type.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type TypeOfArray = T extends (infer U)[] ? U : T; - -export type ValueOf = T[keyof T]; diff --git a/libs/json-api/json-shared-type/tsconfig.json b/libs/json-api/json-shared-type/tsconfig.json deleted file mode 100644 index 8122543a..00000000 --- a/libs/json-api/json-shared-type/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/libs/json-api/json-shared-type/tsconfig.lib.json b/libs/json-api/json-shared-type/tsconfig.lib.json deleted file mode 100644 index 4befa7f0..00000000 --- a/libs/json-api/json-shared-type/tsconfig.lib.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "declaration": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/json-api/json-shared-type/tsconfig.spec.json b/libs/json-api/json-shared-type/tsconfig.spec.json deleted file mode 100644 index 69a251f3..00000000 --- a/libs/json-api/json-shared-type/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} diff --git a/libs/shared-utils/.eslintrc.json b/libs/shared-utils/.eslintrc.json deleted file mode 100644 index 9d9c0db5..00000000 --- a/libs/shared-utils/.eslintrc.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - }, - { - "files": ["*.ts", "*.tsx"], - "rules": {} - }, - { - "files": ["*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/libs/shared-utils/README.md b/libs/shared-utils/README.md deleted file mode 100644 index 038826ff..00000000 --- a/libs/shared-utils/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# shared-utils - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test shared-utils` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/shared-utils/jest.config.ts b/libs/shared-utils/jest.config.ts deleted file mode 100644 index f2d2f024..00000000 --- a/libs/shared-utils/jest.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'shared-utils', - preset: '../../jest.preset.js', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/libs/shared-utils', -}; diff --git a/libs/shared-utils/project.json b/libs/shared-utils/project.json deleted file mode 100644 index f67eee36..00000000 --- a/libs/shared-utils/project.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "shared-utils", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/shared-utils/src", - "projectType": "library", - "targets": { - "build1": { - "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/libs/shared-utils", - "tsConfig": "libs/shared-utils/tsconfig.lib.json", - "packageJson": "libs/shared-utils/package.json", - "main": "libs/shared-utils/src/index.ts", - "assets": ["libs/shared-utils/*.md"] - } - }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "options": { - "jestConfig": "libs/shared-utils/jest.config.ts" - } - } - }, - "tags": [] -} diff --git a/libs/shared-utils/src/index.ts b/libs/shared-utils/src/index.ts deleted file mode 100644 index a0fe9b9f..00000000 --- a/libs/shared-utils/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/utils'; -export * from './lib/types'; diff --git a/libs/shared-utils/src/lib/types/index.ts b/libs/shared-utils/src/lib/types/index.ts deleted file mode 100644 index c0f762d2..00000000 --- a/libs/shared-utils/src/lib/types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './utils-string.type'; diff --git a/libs/shared-utils/src/lib/types/utils-string.type.ts b/libs/shared-utils/src/lib/types/utils-string.type.ts deleted file mode 100644 index d5719e31..00000000 --- a/libs/shared-utils/src/lib/types/utils-string.type.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type KebabCase = S extends `${infer C}${infer T}` - ? KebabCase extends infer U - ? U extends string - ? T extends Uncapitalize - ? `${Uncapitalize}${U}` - : `${Uncapitalize}-${U}` - : never - : never - : S; - -export type KebabToCamelCase = - S extends `${infer T}-${infer U}-${infer V}` - ? `${T}${Capitalize}${Capitalize>}` - : S extends `${infer T}-${infer U}` - ? `${Capitalize}${Capitalize>}` - : S; diff --git a/libs/shared-utils/src/lib/utils/index.ts b/libs/shared-utils/src/lib/utils/index.ts deleted file mode 100644 index a7799257..00000000 --- a/libs/shared-utils/src/lib/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './string-utils'; -export * from './object-utils'; diff --git a/libs/shared-utils/src/lib/utils/object-utils.ts b/libs/shared-utils/src/lib/utils/object-utils.ts deleted file mode 100644 index 461455b3..00000000 --- a/libs/shared-utils/src/lib/utils/object-utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -export const ObjectTyped = { - keys: Object.keys as (yourObject: T) => Array, - values: Object.values as (yourObject: U) => Array, - entries: Object.entries as ( - yourObject: O - ) => Array<[keyof O, O[keyof O]]>, - fromEntries: Object.fromEntries as ( - yourObjectEntries: [K, V][] - ) => Record, -}; - -export function isObject(item: unknown): item is object { - return typeof item === 'object' && !Array.isArray(item) && item !== null; -} diff --git a/libs/shared-utils/src/lib/utils/string-utils.spec.ts b/libs/shared-utils/src/lib/utils/string-utils.spec.ts deleted file mode 100644 index 437de652..00000000 --- a/libs/shared-utils/src/lib/utils/string-utils.spec.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { camelToKebab, snakeToCamel, isString, kebabToCamel } from './'; - -describe('Test utils', () => { - it('camelToKebab', () => { - const result = camelToKebab('ApproverGroups'); - const result1 = camelToKebab('Users'); - - expect(result).toBe('approver-groups'); - expect(result1).toBe('users'); - }); - - it('snakeToCamel', () => { - const result = snakeToCamel('test_test'); - const result1 = snakeToCamel('test-test'); - const result2 = snakeToCamel('testTest'); - const result3 = snakeToCamel('event_incident_typeFK'); - expect(result).toBe('testTest'); - expect(result1).toBe('testTest'); - expect(result2).toBe('testTest'); - expect(result3).toBe('eventIncidentTypeFK'); - }); - - it('isString', () => { - expect(isString('string')).toBe(true); - expect(isString(String('string'))).toBe(true); - expect(isString(new Date())).toBe(false); - expect(isString(class {})).toBe(false); - }); - - it('kebabToCamel', () => { - const type = 'users-group'; - const type1 = 'users'; - - expect(kebabToCamel(type)).toBe('UsersGroup'); - expect(kebabToCamel(type1)).toBe('Users'); - }); -}); diff --git a/libs/shared-utils/src/lib/utils/string-utils.ts b/libs/shared-utils/src/lib/utils/string-utils.ts deleted file mode 100644 index 3b678710..00000000 --- a/libs/shared-utils/src/lib/utils/string-utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { KebabToCamelCase, KebabCase } from '../types'; - -export function isString(value: T): value is P { - return typeof value === 'string' || value instanceof String; -} - -export function snakeToCamel(str: string): string { - if (!str.match(/[\s_-]/g)) { - return str; - } - return str.replace(/([-_][a-z])/g, (group) => - group.toUpperCase().replace('-', '').replace('_', '') - ); -} - -export function camelToKebab(string: S): KebabCase { - return string - .replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1') - .toLowerCase() as KebabCase; -} - -export function upperFirstLetter(string: S): Capitalize { - return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize; -} - -export function kebabToCamel(str: S): KebabToCamelCase { - return str - .split('-') - .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) - .join('') as KebabToCamelCase; -} - -export function capitalizeFirstChar(str: string) { - return str - .split('-') - .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) - .join(''); -} diff --git a/libs/shared-utils/tsconfig.json b/libs/shared-utils/tsconfig.json deleted file mode 100644 index f5b85657..00000000 --- a/libs/shared-utils/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "module": "commonjs", - "forceConsistentCasingInFileNames": true, - "strict": true, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true - }, - "files": [], - "include": [], - "references": [ - { - "path": "./tsconfig.lib.json" - }, - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/libs/shared-utils/tsconfig.lib.json b/libs/shared-utils/tsconfig.lib.json deleted file mode 100644 index 33eca2c2..00000000 --- a/libs/shared-utils/tsconfig.lib.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "declaration": true, - "types": ["node"] - }, - "include": ["src/**/*.ts"], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] -} diff --git a/libs/shared-utils/tsconfig.spec.json b/libs/shared-utils/tsconfig.spec.json deleted file mode 100644 index 9b2a121d..00000000 --- a/libs/shared-utils/tsconfig.spec.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "../../dist/out-tsc", - "module": "commonjs", - "types": ["jest", "node"] - }, - "include": [ - "jest.config.ts", - "src/**/*.test.ts", - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] -} From 792bf2cea4fd43630d62392394e45f41e86af158 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 06:49:09 +0100 Subject: [PATCH 07/26] refactor(json-api-nestjs): Deep refactoring BREAKING CHANGE: Now relation body params allow only as specification. https://jsonapi.org/format/#crud-updating-resource-relationships befoare allow without data props --- .verdaccio/config.yml | 28 + docker-compose.yaml | 51 ++ .../src/lib/service/json-api-utils.service.ts | 2 +- .../src/lib/types/query-params.ts | 2 +- .../json-api-nestjs-shared/.eslintrc.json | 30 + .../json-api/json-api-nestjs-shared/README.md | 11 + .../json-api-nestjs-shared/jest.config.ts | 10 + .../json-api-nestjs-shared/package.json | 10 + .../json-api-nestjs-shared/project.json | 33 + .../json-api-nestjs-shared/src/index.ts | 2 + .../src/lib/types/entity-type.ts | 17 + .../src/lib/types/index.ts | 4 + .../src/lib/types/query-type.ts | 36 + .../src/lib/types/response-body.ts | 69 ++ .../src/lib/types/utils-string.type.ts | 20 + .../src/lib/utils/index.ts | 2 + .../src/lib/utils/object-utils.ts | 14 + .../src/lib/utils/string-utils.spec.ts | 42 + .../src/lib/utils/string-utils.ts | 38 + .../json-api-nestjs-shared/tsconfig.json | 22 + .../json-api-nestjs-shared/tsconfig.lib.json | 16 + .../json-api-nestjs-shared/tsconfig.spec.json | 15 + libs/json-api/json-api-nestjs/.eslintrc.json | 11 +- libs/json-api/json-api-nestjs/project.json | 67 +- libs/json-api/json-api-nestjs/src/index.ts | 23 + .../src/lib/constants/default.ts | 4 + .../json-api-nestjs/src/lib/constants/di.ts | 28 + .../src/lib/constants/index.ts | 14 + .../src/lib/constants/reflection.ts | 2 + .../src/lib/json-api.module.ts | 32 + .../src/lib/mock-utils/db-for-test | 647 +++++++++++++++ .../src/lib/mock-utils/entities/addresses.ts | 69 ++ .../src/lib/mock-utils/entities/comments.ts | 57 ++ .../src/lib/mock-utils/entities/index.ts | 29 + .../src/lib/mock-utils/entities/notes.ts | 44 + .../src/lib/mock-utils/entities/pods.ts | 45 + .../entities/requests-have-pod-locks.ts | 91 +++ .../src/lib/mock-utils/entities/requests.ts | 48 ++ .../src/lib/mock-utils/entities/roles.ts | 58 ++ .../lib/mock-utils/entities/user-groups.ts | 20 + .../src/lib/mock-utils/entities/users.ts | 133 +++ .../src/lib/mock-utils/index.ts | 94 +++ .../src/lib/mock-utils/utils/index.ts | 2 + .../lib/mock-utils/utils/provider-entities.ts | 69 ++ .../src/lib/mock-utils/utils/pull-data.ts | 122 +++ .../atomic-operation.module.ts | 71 ++ .../atomic-operation/constants/index.ts | 10 + .../atomic-operation/controllers/index.ts | 1 + .../controllers/operation.controller.spec.ts | 220 +++++ .../controllers/operation.controller.ts | 104 +++ .../factory/async-iterator.ts | 75 ++ .../modules/atomic-operation/factory/index.ts | 4 + .../factory/map-controller-entity.ts | 25 + .../factory/map-entity-name-to-entity.ts | 19 + .../factory/zod-input-operation.ts | 22 + .../src/lib/modules/atomic-operation/index.ts | 1 + .../pipes/input-operation.pipe.spec.ts | 75 ++ .../pipes/input-operation.pipe.ts | 32 + .../service/execute.service.spec.ts | 609 ++++++++++++++ .../service/execute.service.ts | 338 ++++++++ .../service/explorer.service.spec.ts | 97 +++ .../service/explorer.service.ts | 109 +++ .../modules/atomic-operation/service/index.ts | 3 + .../service/swagger.service.ts | 97 +++ .../modules/atomic-operation/types/index.ts | 33 + .../modules/atomic-operation/utils/index.ts | 1 + .../utils/zod/zod-helper.spec.ts | 591 ++++++++++++++ .../atomic-operation/utils/zod/zod-helper.ts | 176 ++++ .../json-api-nestjs/src/lib/modules/index.ts | 3 + .../src/lib/modules/micro-orm/index.ts | 2 + .../lib/modules/micro-orm/micro-orm.module.ts | 14 + .../src/lib/modules/micro-orm/type.ts | 3 + .../lib/modules/mixin/config/bindings.spec.ts | 9 + .../src/lib/modules/mixin/config/bindings.ts | 202 +++++ .../mixin/controller/json-base.controller.ts | 79 ++ .../src/lib/modules/mixin/decorators/index.ts | 2 + .../inject-service.decorator.spec.ts | 30 + .../inject-service.decorator.ts | 7 + .../json-api/json-api.decorator.spec.ts | 69 ++ .../decorators/json-api/json-api.decorator.ts | 19 + .../src/lib/modules/mixin/factory/index.ts | 1 + .../mixin/factory/zod-validate.factory.ts | 160 ++++ .../mixin/helper/bind-controller.spec.ts | 165 ++++ .../modules/mixin/helper/bind-controller.ts | 124 +++ .../mixin/helper/create-controller.spec.ts | 95 +++ .../modules/mixin/helper/create-controller.ts | 53 ++ .../src/lib/modules/mixin/helper/index.ts | 3 + .../lib/modules/mixin/helper/utils.spec.ts | 17 + .../src/lib/modules/mixin/helper/utils.ts | 41 + .../mixin/interceptors/error.interceptors.ts | 120 +++ .../lib/modules/mixin/interceptors/index.ts | 2 + .../interceptors/log-time.interceptors.ts | 30 + .../src/lib/modules/mixin/mixin.module.ts | 84 ++ .../check-item-entity.pipe.spec.ts | 55 ++ .../check-item-entity.pipe.ts | 34 + .../mixin/pipe/check-item-entity/index.ts | 1 + .../src/lib/modules/mixin/pipe/index.spec.ts | 46 ++ .../src/lib/modules/mixin/pipe/index.ts | 89 ++ .../pipe/parse-relationship-name/index.ts | 1 + .../parse-relationship-name.pipe.spec.ts | 58 ++ .../parse-relationship-name.pipe.ts | 38 + .../modules/mixin/pipe/patch-input/index.ts | 1 + .../pipe/patch-input/patch-input.pipe.spec.ts | 69 ++ .../pipe/patch-input/patch-input.pipe.ts | 33 + .../mixin/pipe/patch-relationship/index.ts | 1 + .../patch-relationship.pipe.spec.ts | 84 ++ .../patch-relationship.pipe.ts | 32 + .../modules/mixin/pipe/post-input/index.ts | 1 + .../pipe/post-input/post-input.pipe.spec.ts | 69 ++ .../mixin/pipe/post-input/post-input.pipe.ts | 32 + .../mixin/pipe/post-relationship/index.ts | 1 + .../post-relationship.pipe.spec.ts | 83 ++ .../post-relationship.pipe.ts | 32 + .../pipe/query-check-select-field/index.ts | 1 + .../query-check-select-field.spec.ts | 73 ++ .../query-check-select-field.ts | 25 + .../pipe/query-filed-on-include/index.ts | 1 + .../query-filed-in-include.pipe.spec.ts | 121 +++ .../query-filed-in-include.pipe.ts | 70 ++ .../modules/mixin/pipe/query-input/index.ts | 1 + .../pipe/query-input/query-input.pipe.spec.ts | 65 ++ .../pipe/query-input/query-input.pipe.ts | 33 + .../src/lib/modules/mixin/pipe/query/index.ts | 1 + .../mixin/pipe/query/query.pipe.spec.ts | 107 +++ .../modules/mixin/pipe/query/query.pipe.ts | 30 + .../lib/modules/mixin/types/binding.types.ts | 47 ++ .../mixin/types/decorator-options.types.ts | 11 + .../src/lib/modules/mixin/types/index.ts | 6 + .../lib/modules/mixin/types/module.types.ts | 28 + .../modules/mixin/types/orm-service.type.ts | 55 ++ .../src/lib/modules/mixin/types/utils.ts | 68 ++ .../src/lib/modules/mixin/types/zod-types.ts | 141 ++++ .../src/lib/modules/mixin/zod/index.ts | 6 + .../index.spec.ts | 42 + .../index.ts | 13 + .../zod/zod-input-patch-schema/index.spec.ts | 220 +++++ .../mixin/zod/zod-input-patch-schema/index.ts | 111 +++ .../index.spec.ts | 43 + .../index.ts | 13 + .../zod/zod-input-post-schema/index.spec.ts | 219 +++++ .../mixin/zod/zod-input-post-schema/index.ts | 109 +++ .../zod/zod-input-query-schema/fields.spec.ts | 111 +++ .../zod/zod-input-query-schema/fields.ts | 59 ++ .../zod/zod-input-query-schema/filter.spec.ts | 123 +++ .../zod/zod-input-query-schema/filter.ts | 196 +++++ .../zod-input-query-schema/include.spec.ts | 32 + .../zod/zod-input-query-schema/include.ts | 18 + .../zod/zod-input-query-schema/index.spec.ts | 136 ++++ .../mixin/zod/zod-input-query-schema/index.ts | 39 + .../zod/zod-input-query-schema/sort.spec.ts | 56 ++ .../mixin/zod/zod-input-query-schema/sort.ts | 37 + .../mixin/zod/zod-query-schema/fields.spec.ts | 160 ++++ .../mixin/zod/zod-query-schema/fields.ts | 53 ++ .../mixin/zod/zod-query-schema/filter.spec.ts | 381 +++++++++ .../mixin/zod/zod-query-schema/filter.ts | 243 ++++++ .../zod/zod-query-schema/include.spec.ts | 53 ++ .../mixin/zod/zod-query-schema/include.ts | 22 + .../mixin/zod/zod-query-schema/index.spec.ts | 238 ++++++ .../mixin/zod/zod-query-schema/index.ts | 99 +++ .../mixin/zod/zod-query-schema/sort.spec.ts | 126 +++ .../mixin/zod/zod-query-schema/sort.ts | 60 ++ .../mixin/zod/zod-share/attributes.spec.ts | 200 +++++ .../modules/mixin/zod/zod-share/attributes.ts | 202 +++++ .../modules/mixin/zod/zod-share/id.spec.ts | 37 + .../src/lib/modules/mixin/zod/zod-share/id.ts | 16 + .../lib/modules/mixin/zod/zod-share/index.ts | 6 + .../modules/mixin/zod/zod-share/page.spec.ts | 42 + .../lib/modules/mixin/zod/zod-share/page.ts | 23 + .../mixin/zod/zod-share/rel-data.spec.ts | 37 + .../modules/mixin/zod/zod-share/rel-data.ts | 32 + .../mixin/zod/zod-share/relationships.spec.ts | 270 ++++++ .../mixin/zod/zod-share/relationships.ts | 157 ++++ .../modules/mixin/zod/zod-share/type.spec.ts | 21 + .../lib/modules/mixin/zod/zod-share/type.ts | 8 + .../lib/modules/mixin/zod/zod-utils.spec.ts | 359 ++++++++ .../src/lib/modules/mixin/zod/zod-utils.ts | 69 ++ .../src/lib/modules/type-orm/constants/di.ts | 2 + .../lib/modules/type-orm/constants/index.ts | 4 + .../src/lib/modules/type-orm/factory/index.ts | 218 +++++ .../src/lib/modules/type-orm/index.ts | 2 + .../modules/type-orm/orm-helper/index.spec.ts | 283 +++++++ .../lib/modules/type-orm/orm-helper/index.ts | 294 +++++++ .../orm-methods/delete-one/delete-one.spec.ts | 71 ++ .../orm-methods/delete-one/delete-one.ts | 21 + .../delete-relationship.spec.ts | 190 +++++ .../delete-relationship.ts | 32 + .../orm-methods/get-all/get-all.spec.ts | 406 +++++++++ .../type-orm/orm-methods/get-all/get-all.ts | 264 ++++++ .../orm-methods/get-one/get-one.spec.ts | 191 +++++ .../type-orm/orm-methods/get-one/get-one.ts | 95 +++ .../get-relationship/get-relationship.spec.ts | 131 +++ .../get-relationship/get-relationship.ts | 71 ++ .../lib/modules/type-orm/orm-methods/index.ts | 21 + .../orm-methods/patch-one/patch-one.spec.ts | 308 +++++++ .../orm-methods/patch-one/patch-one.ts | 73 ++ .../patch-relationship.spec.ts | 230 ++++++ .../patch-relationship/patch-relationship.ts | 51 ++ .../orm-methods/post-one/post-one.spec.ts | 256 ++++++ .../type-orm/orm-methods/post-one/post-one.ts | 39 + .../post-relationship.spec.ts | 205 +++++ .../post-relationship/post-relationship.ts | 40 + .../service/entity-props-map.service.spec.ts | 85 ++ .../service/entity-props-map.service.ts | 102 +++ .../src/lib/modules/type-orm/service/index.ts | 4 + .../service/transform-data.service.spec.ts | 372 +++++++++ .../service/transform-data.service.ts | 259 ++++++ .../type-orm/service/type-orm.service.ts | 135 +++ .../service/typeorm-utils.service.spec.ts | 770 ++++++++++++++++++ .../type-orm/service/typeorm-utils.service.ts | 685 ++++++++++++++++ .../lib/modules/type-orm/type-orm.module.ts | 67 ++ .../src/lib/modules/type-orm/type.ts | 11 + .../src/lib/types/config-param.ts | 45 + .../src/lib/types/error.types.ts | 18 + .../json-api-nestjs/src/lib/types/index.ts | 5 + .../src/lib/types/module-common.types.ts | 29 + .../json-api-nestjs/src/lib/types/operand.ts | 26 + .../src/lib/types/util-types.ts | 24 + .../src/lib/utils/helper.spec.ts | 152 ++++ .../json-api-nestjs/src/lib/utils/helper.ts | 120 +++ .../json-api-nestjs/src/lib/utils/index.ts | 1 + migrations.json | 142 ++++ project.json | 14 + 222 files changed, 18878 insertions(+), 57 deletions(-) create mode 100644 .verdaccio/config.yml create mode 100644 docker-compose.yaml create mode 100644 libs/json-api/json-api-nestjs-shared/.eslintrc.json create mode 100644 libs/json-api/json-api-nestjs-shared/README.md create mode 100644 libs/json-api/json-api-nestjs-shared/jest.config.ts create mode 100644 libs/json-api/json-api-nestjs-shared/package.json create mode 100644 libs/json-api/json-api-nestjs-shared/project.json create mode 100644 libs/json-api/json-api-nestjs-shared/src/index.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts create mode 100644 libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts create mode 100644 libs/json-api/json-api-nestjs-shared/tsconfig.json create mode 100644 libs/json-api/json-api-nestjs-shared/tsconfig.lib.json create mode 100644 libs/json-api/json-api-nestjs-shared/tsconfig.spec.json create mode 100644 libs/json-api/json-api-nestjs/src/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/default.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/di.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/json-api.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/config-param.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/error.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/operand.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/types/util-types.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/helper.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/index.ts create mode 100644 migrations.json create mode 100644 project.json diff --git a/.verdaccio/config.yml b/.verdaccio/config.yml new file mode 100644 index 00000000..f74420f2 --- /dev/null +++ b/.verdaccio/config.yml @@ -0,0 +1,28 @@ +# path to a directory with all packages +storage: ../tmp/local-registry/storage + +# a list of other known repositories we can talk to +uplinks: + npmjs: + url: https://registry.npmjs.org/ + maxage: 60m + +packages: + '**': + # give all users (including non-authenticated users) full access + # because it is a local registry + access: $all + publish: $all + unpublish: $all + + # if package is not available locally, proxy requests to npm registry + proxy: npmjs + +# log settings +log: + type: stdout + format: pretty + level: warn + +publish: + allow_offline: true # set offline to true to allow publish offline diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..52340133 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,51 @@ +version: '3.8' +services: +# api-gate: +# container_name: api-gate +# restart: always +# build: apps/api-gate/ +# ports: +# - "3000:3000" + postgres: + image: postgres:15.1-alpine + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + ports: + - '5432:5432' + volumes: + - db:/var/lib/postgresql/data + pgadmin: + container_name: 'pgadmin' + image: 'dpage/pgadmin4:latest' + hostname: pgadmin + depends_on: + - 'postgres' + environment: + - PGADMIN_DEFAULT_PASSWORD=password + - PGADMIN_DEFAULT_EMAIL=pg.admin@email.com + volumes: + - pgadmin:/root/.pgadmin + ports: + - '8000:80' +# redis: +# image: redis:6.2-alpine +# restart: always +# ports: +# - '6379:6379' +# command: redis-server --save 20 1 --loglevel warning +# volumes: +# - redis:/data +# jaeger: +# image: jaegertracing/all-in-one:1.41 +# ports: +# - "16686:16686" +# - "14268:14268" +volumes: + db: + driver: local +# redis: +# driver: local + pgadmin: + driver: local diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts index 0584fd3c..b85cd65f 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts @@ -338,7 +338,7 @@ export class JsonApiUtilsService { } return { ...acum, - [key]: data, + [key]: { data }, }; }, {} as Relationships); diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts index 1450e19d..c88c995a 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts @@ -1,4 +1,4 @@ -import { QueryField } from 'json-shared-type'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; import { EntityProps, EntityRelation } from './entity'; import { TypeOfArray } from './utils'; import { Operands, OperandsRelation } from './filter-operand'; diff --git a/libs/json-api/json-api-nestjs-shared/.eslintrc.json b/libs/json-api/json-api-nestjs-shared/.eslintrc.json new file mode 100644 index 00000000..0af28030 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/.eslintrc.json @@ -0,0 +1,30 @@ +{ + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"] + } + ] + } + } + ] +} diff --git a/libs/json-api/json-api-nestjs-shared/README.md b/libs/json-api/json-api-nestjs-shared/README.md new file mode 100644 index 00000000..8703f1bf --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/README.md @@ -0,0 +1,11 @@ +# json-api-nestjs-shared + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build json-api-nestjs-shared` to build the library. + +## Running unit tests + +Run `nx test json-api-nestjs-shared` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/json-api/json-api-nestjs-shared/jest.config.ts b/libs/json-api/json-api-nestjs-shared/jest.config.ts new file mode 100644 index 00000000..67ade3e9 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/jest.config.ts @@ -0,0 +1,10 @@ +export default { + displayName: 'json-api-nestjs-shared', + preset: '../../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../../coverage/libs/json-api/json-api-nestjs-shared', +}; diff --git a/libs/json-api/json-api-nestjs-shared/package.json b/libs/json-api/json-api-nestjs-shared/package.json new file mode 100644 index 00000000..25918d9b --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/package.json @@ -0,0 +1,10 @@ +{ + "name": "@klerick/json-api-nestjs-shared", + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "type": "commonjs", + "main": "./src/index.js", + "typings": "./src/index.d.ts" +} diff --git a/libs/json-api/json-api-nestjs-shared/project.json b/libs/json-api/json-api-nestjs-shared/project.json new file mode 100644 index 00000000..cdb32619 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/project.json @@ -0,0 +1,33 @@ +{ + "name": "json-api-nestjs-shared", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/json-api/json-api-nestjs-shared/src", + "projectType": "library", + "release": { + "version": { + "generatorOptions": { + "packageRoot": "dist/{projectRoot}", + "currentVersionResolver": "git-tag" + } + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/json-api/json-api-nestjs-shared", + "tsConfig": "libs/json-api/json-api-nestjs-shared/tsconfig.lib.json", + "packageJson": "libs/json-api/json-api-nestjs-shared/package.json", + "main": "libs/json-api/json-api-nestjs-shared/src/index.ts", + "assets": ["libs/json-api/json-api-nestjs-shared/*.md"] + } + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + } + } +} diff --git a/libs/json-api/json-api-nestjs-shared/src/index.ts b/libs/json-api/json-api-nestjs-shared/src/index.ts new file mode 100644 index 00000000..a0fe9b9f --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/utils'; +export * from './lib/types'; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts new file mode 100644 index 00000000..5fbdd04f --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/entity-type.ts @@ -0,0 +1,17 @@ +export type EntityField = + | string + | number + | bigint + | boolean + | string[] + | number[] + | null + | Date; + +export type EntityProps = { + [P in keyof T]: T[P] extends EntityField ? P : never; +}[keyof T]; + +export type EntityRelation = { + [P in keyof T]: T[P] extends EntityField ? never : P; +}[keyof T]; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts new file mode 100644 index 00000000..33b70ba6 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/index.ts @@ -0,0 +1,4 @@ +export * from './utils-string.type'; +export * from './query-type'; +export * from './entity-type'; +export * from './response-body'; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts new file mode 100644 index 00000000..ddeead7a --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/query-type.ts @@ -0,0 +1,36 @@ +export enum QueryField { + filter = 'filter', + sort = 'sort', + include = 'include', + page = 'page', + fields = 'fields', +} + +export enum FilterOperand { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', + in = 'in', + nin = 'nin', + some = 'some', +} + +export enum FilterOperandOnlyInNin { + in = 'in', + nin = 'nin', +} +export enum FilterOperandOnlySimple { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', +} diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts new file mode 100644 index 00000000..fbe91dde --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts @@ -0,0 +1,69 @@ +import { + EntityField, + EntityProps, + EntityRelation, + TypeOfArray, + ValueOf, +} from '.'; + +export type PageProps = { + totalItems: number; + pageNumber: number; + pageSize: number; +}; + +export type DebugMetaProps = Partial<{ + time: number; +}>; + +export type MainData = { + type: T; + id: string; +}; + +export type Links = { + self: string; + related?: string; +}; + +export type Attributes = { + [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; +}; + +export type Data = { + data?: E extends unknown[] ? MainData[] : MainData | null; +}; + +export type Relationships = { + [P in EntityRelation]?: { + links: Links; + } & Data; +}; + +export type Include = ValueOf<{ + [P in EntityRelation]: ResourceData>; +}>; + +export type ResourceData = MainData & { + attributes?: Attributes; + relationships?: Relationships; + links: Omit; +}; + +export type MetaProps = R extends null ? T : T & R; + +export type ResourceObject< + T, + R extends 'object' | 'array' = 'object', + M = null +> = { + meta: R extends 'array' + ? MetaProps + : MetaProps; + data: R extends 'array' ? ResourceData[] : ResourceData; + included?: Include[]; +}; + +export type ResourceObjectRelationships> = { + meta: DebugMetaProps; +} & Required>; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts new file mode 100644 index 00000000..2d4284b2 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts @@ -0,0 +1,20 @@ +export type KebabCase = S extends `${infer C}${infer T}` + ? KebabCase extends infer U + ? U extends string + ? T extends Uncapitalize + ? `${Uncapitalize}${U}` + : `${Uncapitalize}-${U}` + : never + : never + : S; + +export type KebabToCamelCase = + S extends `${infer T}-${infer U}-${infer V}` + ? `${T}${Capitalize}${Capitalize>}` + : S extends `${infer T}-${infer U}` + ? `${Capitalize}${Capitalize>}` + : S; + +export type TypeOfArray = T extends (infer U)[] ? U : T; + +export type ValueOf = T[keyof T]; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts new file mode 100644 index 00000000..a7799257 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/index.ts @@ -0,0 +1,2 @@ +export * from './string-utils'; +export * from './object-utils'; diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts new file mode 100644 index 00000000..461455b3 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts @@ -0,0 +1,14 @@ +export const ObjectTyped = { + keys: Object.keys as (yourObject: T) => Array, + values: Object.values as (yourObject: U) => Array, + entries: Object.entries as ( + yourObject: O + ) => Array<[keyof O, O[keyof O]]>, + fromEntries: Object.fromEntries as ( + yourObjectEntries: [K, V][] + ) => Record, +}; + +export function isObject(item: unknown): item is object { + return typeof item === 'object' && !Array.isArray(item) && item !== null; +} diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts new file mode 100644 index 00000000..4596cd21 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.spec.ts @@ -0,0 +1,42 @@ +import { + camelToKebab, + snakeToCamel, + isString, + kebabToCamel, +} from './string-utils'; + +describe('Test utils', () => { + it('camelToKebab', () => { + const result = camelToKebab('ApproverGroups'); + const result1 = camelToKebab('Users'); + + expect(result).toBe('approver-groups'); + expect(result1).toBe('users'); + }); + + it('snakeToCamel', () => { + const result = snakeToCamel('test_test'); + const result1 = snakeToCamel('test-test'); + const result2 = snakeToCamel('testTest'); + const result3 = snakeToCamel('event_incident_typeFK'); + expect(result).toBe('testTest'); + expect(result1).toBe('testTest'); + expect(result2).toBe('testTest'); + expect(result3).toBe('eventIncidentTypeFK'); + }); + + it('isString', () => { + expect(isString('string')).toBe(true); + expect(isString(String('string'))).toBe(true); + expect(isString(new Date())).toBe(false); + expect(isString(class {})).toBe(false); + }); + + it('kebabToCamel', () => { + const type = 'users-group'; + const type1 = 'users'; + + expect(kebabToCamel(type)).toBe('UsersGroup'); + expect(kebabToCamel(type1)).toBe('Users'); + }); +}); diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts new file mode 100644 index 00000000..3b678710 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/string-utils.ts @@ -0,0 +1,38 @@ +import { KebabToCamelCase, KebabCase } from '../types'; + +export function isString(value: T): value is P { + return typeof value === 'string' || value instanceof String; +} + +export function snakeToCamel(str: string): string { + if (!str.match(/[\s_-]/g)) { + return str; + } + return str.replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace('-', '').replace('_', '') + ); +} + +export function camelToKebab(string: S): KebabCase { + return string + .replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1') + .toLowerCase() as KebabCase; +} + +export function upperFirstLetter(string: S): Capitalize { + return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize; +} + +export function kebabToCamel(str: S): KebabToCamelCase { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join('') as KebabToCamelCase; +} + +export function capitalizeFirstChar(str: string) { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join(''); +} diff --git a/libs/json-api/json-api-nestjs-shared/tsconfig.json b/libs/json-api/json-api-nestjs-shared/tsconfig.json new file mode 100644 index 00000000..0dc79caa --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/json-api/json-api-nestjs-shared/tsconfig.lib.json b/libs/json-api/json-api-nestjs-shared/tsconfig.lib.json new file mode 100644 index 00000000..dbf54fd7 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/json-api/json-api-nestjs-shared/tsconfig.spec.json b/libs/json-api/json-api-nestjs-shared/tsconfig.spec.json new file mode 100644 index 00000000..ab55b7c7 --- /dev/null +++ b/libs/json-api/json-api-nestjs-shared/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/json-api/json-api-nestjs/.eslintrc.json b/libs/json-api/json-api-nestjs/.eslintrc.json index ac1a6002..0af28030 100644 --- a/libs/json-api/json-api-nestjs/.eslintrc.json +++ b/libs/json-api/json-api-nestjs/.eslintrc.json @@ -1,6 +1,6 @@ { - "extends": ["../../../.eslintrc.json"], - "ignorePatterns": ["!**/*", "**/*.spec.ts"], + "extends": ["../../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], @@ -18,7 +18,12 @@ "files": ["*.json"], "parser": "jsonc-eslint-parser", "rules": { - "@nx/dependency-checks": "error" + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": ["{projectRoot}/eslint.config.{js,cjs,mjs}"] + } + ] } } ] diff --git a/libs/json-api/json-api-nestjs/project.json b/libs/json-api/json-api-nestjs/project.json index f0cb4555..be561184 100644 --- a/libs/json-api/json-api-nestjs/project.json +++ b/libs/json-api/json-api-nestjs/project.json @@ -3,68 +3,31 @@ "$schema": "../../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "libs/json-api/json-api-nestjs/src", "projectType": "library", + "release": { + "version": { + "generatorOptions": { + "packageRoot": "dist/{projectRoot}", + "currentVersionResolver": "git-tag" + } + } + }, + "tags": [], "targets": { - "build-ts": { + "build": { "executor": "@nx/js:tsc", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/libs/json-api/json-api-nestjs", - "main": "libs/json-api/json-api-nestjs/src/index.ts", "tsConfig": "libs/json-api/json-api-nestjs/tsconfig.lib.json", - "assets": ["libs/json-api/json-api-nestjs/README.md"], - "external": "none", - "updateBuildableProjectDepsInPackageJson": true, - "buildableProjectDepsInPackageJsonType": "peerDependencies", - "generateExportsField": true - } - }, - "build": { - "executor": "nx:run-commands", - "dependsOn": [ - "build-ts" - ], - "options": { - "commands": ["rm -rf dist/libs/json-api/json-api-nestjs/libs"], - "cwd": "./", - "parallel": false - } - }, - "publish": { - "command": "node tools/scripts/publish.mjs json-api-nestjs {args.ver} {args.tag}", - "dependsOn": ["build"] - }, - "lint": { - "executor": "@nx/eslint:lint", - "outputs": ["{options.outputFile}"] - }, - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], - "options": { - "jestConfig": "libs/json-api/json-api-nestjs/jest.config.ts", - "codeCoverage": true, - "coverageReporters": ["json-summary"] - } - }, - "upload-badge": { - "executor": "nx:run-commands", - "dependsOn": [ - { - "target": "test" - } - ], - "options": { - "commands": ["node tools/scripts/upload-badge.mjs json-api-nestjs"], - "cwd": "./", - "parallel": false, - "outputPath": "{workspaceRoot}/libs/json-api/json-api-nestjs" + "packageJson": "libs/json-api/json-api-nestjs/package.json", + "main": "libs/json-api/json-api-nestjs/src/index.ts", + "assets": ["libs/json-api/json-api-nestjs/*.md"] } }, "nx-release-publish": { "options": { - "packageRoot": "dist/libs/json-api/json-api-nestjs" + "packageRoot": "dist/{projectRoot}" } } - }, - "tags": [] + } } diff --git a/libs/json-api/json-api-nestjs/src/index.ts b/libs/json-api/json-api-nestjs/src/index.ts new file mode 100644 index 00000000..e8d912cd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/index.ts @@ -0,0 +1,23 @@ +export { JsonApiModule } from './lib/json-api.module'; +export { TypeOrmModule, MicroOrmModule } from './lib/modules'; + +export { JsonApi, InjectService } from './lib/modules/mixin/decorators'; +export { OrmService as JsonApiService } from './lib/modules/mixin/types'; +export { JsonBaseController } from './lib/modules/mixin/controller/json-base.controller'; +export { + Query, + PatchData, + PostData, + PostRelationshipData, + PatchRelationshipData, +} from './lib/modules/mixin/zod'; + +export { + EntityRelation, + ResourceObject, + ResourceObjectRelationships, + QueryField, +} from '@klerick/json-api-nestjs-shared'; + +export { excludeMethod } from './lib/modules/mixin/config/bindings'; +export { entityForClass } from './lib/utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/default.ts b/libs/json-api/json-api-nestjs/src/lib/constants/default.ts new file mode 100644 index 00000000..de75ca27 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/default.ts @@ -0,0 +1,4 @@ +export const DEFAULT_CONNECTION_NAME = 'default'; + +export const DEFAULT_QUERY_PAGE = 1; +export const DEFAULT_PAGE_SIZE = 20; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/di.ts b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts new file mode 100644 index 00000000..ec21865e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts @@ -0,0 +1,28 @@ +export const CURRENT_ENTITY_MANAGER_TOKEN = Symbol( + 'CURRENT_ENTITY_MANAGER_TOKEN' +); +export const GLOBAL_MODULE_OPTIONS_TOKEN = Symbol('GLOBAL_MODULE_OPTIONS'); +export const ORM_SERVICE = Symbol('ORM_SERVICE'); +export const ORM_SERVICE_PROPS = Symbol('ORM_SERVICE_PROPS'); + +export const PARAMS_FOR_ZOD_SCHEMA = Symbol('PARAMS_FOR_ZOD_SCHEMA'); +export const FIELD_FOR_ENTITY = Symbol('FIELD_FOR_ENTITY'); +export const CONTROL_OPTIONS_TOKEN = Symbol('CONTROL_OPTIONS_TOKEN'); +export const RUN_IN_TRANSACTION_FUNCTION = Symbol( + 'RUN_IN_TRANSACTION_FUNCTION' +); + +export const CURRENT_ENTITY = Symbol('CURRENT_ENTITY'); +export const FIND_ONE_ROW_ENTITY = Symbol('FIND_ONE_ROW_ENTITY'); +export const CHECK_RELATION_NAME = Symbol('CHECK_RELATION_NAME'); + +export const ZOD_INPUT_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); +export const ZOD_QUERY_SCHEMA = Symbol('ZOD_INPUT_QUERY_SCHEMA'); +export const ZOD_POST_SCHEMA = Symbol('ZOD_POST_SCHEMA'); +export const ZOD_PATCH_SCHEMA = Symbol('ZOD_PATCH_SCHEMA'); +export const ZOD_POST_RELATIONSHIP_SCHEMA = Symbol( + 'ZOD_POST_RELATIONSHIP_SCHEMA' +); +export const ZOD_PATCH_RELATIONSHIP_SCHEMA = Symbol( + 'ZOD_PATCH_RELATIONSHIP_SCHEMA' +); diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/constants/index.ts new file mode 100644 index 00000000..83b9d181 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/index.ts @@ -0,0 +1,14 @@ +export * from './default'; +export * from './di'; +export * from './reflection'; + +export const JSON_API_CONTROLLER_POSTFIX = 'JsonApiController'; +export const JSON_API_MODULE_POSTFIX = 'JsonApiModule'; + +export const PARAMS_RESOURCE_ID = 'id'; +export const PARAMS_RELATION_ID = 'relId'; +export const PARAMS_RELATION_NAME = 'relName'; + +export const DESC = 'DESC'; +export const ASC = 'ASC'; +export const SORT_TYPE = [DESC, ASC] as const; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts b/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts new file mode 100644 index 00000000..6656d32f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/constants/reflection.ts @@ -0,0 +1,2 @@ +export const JSON_API_DECORATOR_ENTITY = Symbol('JSON_API_ENTITY'); +export const JSON_API_DECORATOR_OPTIONS = Symbol('JSON_API_OPTIONS'); diff --git a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts new file mode 100644 index 00000000..d14b20fd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts @@ -0,0 +1,32 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { DiscoveryModule } from '@nestjs/core'; + +import { AnyEntity, EntityName, ModuleOptions } from './types'; +import { createMixinModule, prepareConfig, createAtomicModule } from './utils'; + +@Module({}) +export class JsonApiModule { + public static forRoot(options: ModuleOptions): DynamicModule { + const resultOption = prepareConfig(options); + + resultOption.imports.unshift(DiscoveryModule); + + const commonOrmModule = resultOption.type.forRoot(resultOption); + + const entitiesMixinModules = resultOption.entities.map( + (entity: EntityName) => + createMixinModule(entity, resultOption, commonOrmModule) + ); + + const operationModuleImport = createAtomicModule( + resultOption, + entitiesMixinModules, + commonOrmModule + ); + + return { + module: JsonApiModule, + imports: [...operationModuleImport, ...entitiesMixinModules], + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test b/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test new file mode 100644 index 00000000..fa08bc14 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/db-for-test @@ -0,0 +1,647 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 12.5 +-- Dumped by pg_dump version 12.5 + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +create extension "uuid-ossp"; + +-- +-- Name: comment_kind_enum; Type: TYPE; Schema: public; Owner: - +-- + +CREATE TYPE public.comment_kind_enum AS ENUM ( + 'COMMENT', + 'MESSAGE', + 'NOTE' +); + + +SET default_table_access_method = heap; + +-- +-- Name: addresses; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.addresses ( + id integer NOT NULL, + city character varying(70) DEFAULT NULL::character varying, + state character varying(70) DEFAULT NULL::character varying, + country character varying(70) DEFAULT NULL::character varying, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + array_field text[] +); + + +-- +-- Name: addresses_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.addresses_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: addresses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.addresses_id_seq OWNED BY public.addresses.id; + + +-- +-- Name: comments; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.comments ( + id integer NOT NULL, + text text NOT NULL, + kind public.comment_kind_enum NOT NULL, + created_by integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: comments_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.comments_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.comments_id_seq OWNED BY public.comments.id; + + +-- +-- Name: notes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.notes ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + text text NOT NULL, + created_by integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + + +-- +-- Name: migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.migrations ( + id integer NOT NULL, + "timestamp" bigint NOT NULL, + name character varying NOT NULL +); + + +-- +-- Name: migrations_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.migrations_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: migrations_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.migrations_id_seq OWNED BY public.migrations.id; + + +-- +-- Name: pods; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.pods ( + id integer NOT NULL, + name character varying, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: pods_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.pods_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: pods_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.pods_id_seq OWNED BY public.pods.id; + + +-- +-- Name: requests; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.requests ( + id integer NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: requests_have_pod_locks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.requests_have_pod_locks ( + id integer NOT NULL, + request_id integer NOT NULL, + pod_id integer NOT NULL, + external_id integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: requests_have_pod_locks_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.requests_have_pod_locks_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: requests_have_pod_locks_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.requests_have_pod_locks_id_seq OWNED BY public.requests_have_pod_locks.id; + + +-- +-- Name: requests_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.requests_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: requests_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.requests_id_seq OWNED BY public.requests.id; + + +-- +-- Name: roles; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.roles ( + id integer NOT NULL, + name character varying(128) DEFAULT NULL::character varying, + key character varying(128) NOT NULL, + is_default boolean DEFAULT false, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.roles_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.roles_id_seq OWNED BY public.roles.id; + + +-- +-- Name: typeorm_metadata; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.typeorm_metadata ( + type character varying NOT NULL, + database character varying, + schema character varying, + "table" character varying, + name character varying, + value text +); + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users ( + id integer NOT NULL, + login character varying(100) NOT NULL, + first_name character varying, + last_name character varying, + is_active boolean DEFAULT false, + test_real real[], + test_array_null real[], + manager_id integer, + addresses_id integer, + user_groups_id integer, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + test_date timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + +-- +-- Name: user_groups; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.user_groups ( + id integer NOT NULL, + label character varying NOT NULL +); + + +-- +-- Name: users_have_roles; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.users_have_roles ( + id integer NOT NULL, + user_id integer NOT NULL, + role_id integer NOT NULL, + created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP +); + + +-- +-- Name: users_have_roles_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.users_have_roles_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: users_have_roles_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.users_have_roles_id_seq OWNED BY public.users_have_roles.id; + + +-- +-- Name: users_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.users_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: users_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.users_id_seq OWNED BY public.users.id; + + +-- +-- Name: user_groups_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.user_groups_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +-- +-- Name: user_groups_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.user_groups_id_seq OWNED BY public.user_groups.id; + + +-- +-- Name: addresses id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.addresses ALTER COLUMN id SET DEFAULT nextval('public.addresses_id_seq'::regclass); + + +-- +-- Name: comments id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments ALTER COLUMN id SET DEFAULT nextval('public.comments_id_seq'::regclass); + + +-- +-- Name: migrations id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.migrations ALTER COLUMN id SET DEFAULT nextval('public.migrations_id_seq'::regclass); + + +-- +-- Name: pods id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pods ALTER COLUMN id SET DEFAULT nextval('public.pods_id_seq'::regclass); + + +-- +-- Name: requests id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests ALTER COLUMN id SET DEFAULT nextval('public.requests_id_seq'::regclass); + + +-- +-- Name: requests_have_pod_locks id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests_have_pod_locks ALTER COLUMN id SET DEFAULT nextval('public.requests_have_pod_locks_id_seq'::regclass); + + +-- +-- Name: roles id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.roles ALTER COLUMN id SET DEFAULT nextval('public.roles_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users ALTER COLUMN id SET DEFAULT nextval('public.users_id_seq'::regclass); + + +-- +-- Name: users id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_groups ALTER COLUMN id SET DEFAULT nextval('public.user_groups_id_seq'::regclass); + + +-- +-- Name: users_have_roles id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_have_roles ALTER COLUMN id SET DEFAULT nextval('public.users_have_roles_id_seq'::regclass); + + +-- +-- Name: requests PK_0428f484e96f9e6a55955f29b5f; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests + ADD CONSTRAINT "PK_0428f484e96f9e6a55955f29b5f" PRIMARY KEY (id); + + +-- +-- Name: addresses PK_745d8f43d3af10ab8247465e450; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.addresses + ADD CONSTRAINT "PK_745d8f43d3af10ab8247465e450" PRIMARY KEY (id); + + +-- +-- Name: comments PK_8bf68bc960f2b69e818bdb90dcb; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments + ADD CONSTRAINT "PK_8bf68bc960f2b69e818bdb90dcb" PRIMARY KEY (id); + + +-- +-- Name: migrations PK_8c82d7f526340ab734260ea46be; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.migrations + ADD CONSTRAINT "PK_8c82d7f526340ab734260ea46be" PRIMARY KEY (id); + + +-- +-- Name: users_have_roles PK_9bb88c2f9f64bff7570e4108108; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_have_roles + ADD CONSTRAINT "PK_9bb88c2f9f64bff7570e4108108" PRIMARY KEY (id); + + +-- +-- Name: users PK_a3ffb1c0c8416b9fc6f907b7433; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY (id); + + +-- +-- Name: users PK_user_groups; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.user_groups + ADD CONSTRAINT "PK_user_groups" PRIMARY KEY (id); + +-- +-- Name: pods PK_b00bbc2c7fb41627be2b169f0dd; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pods + ADD CONSTRAINT "PK_b00bbc2c7fb41627be2b169f0dd" PRIMARY KEY (id); + + +-- +-- Name: roles PK_c1433d71a4838793a49dcad46ab; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.roles + ADD CONSTRAINT "PK_c1433d71a4838793a49dcad46ab" PRIMARY KEY (id); + + +-- +-- Name: requests_have_pod_locks PK_f214657396a396b70a697b04a85; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests_have_pod_locks + ADD CONSTRAINT "PK_f214657396a396b70a697b04a85" PRIMARY KEY (id); + + +-- +-- Name: users UQ_2d443082eccd5198f95f2a36e2c; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT "UQ_2d443082eccd5198f95f2a36e2c" UNIQUE (login); + + +-- +-- Name: roles UQ_a87cf0659c3ac379b339acf36a2; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.roles + ADD CONSTRAINT "UQ_a87cf0659c3ac379b339acf36a2" UNIQUE (key); + + +-- +-- Name: IDX_48d6a9a1ab3943e6c6d2a25d2e; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX "IDX_48d6a9a1ab3943e6c6d2a25d2e" ON public.requests_have_pod_locks USING btree (request_id, pod_id); + + +-- +-- Name: IDX_61c360686dfe8d62a9b03873bf; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX "IDX_61c360686dfe8d62a9b03873bf" ON public.users_have_roles USING btree (user_id, role_id); + + +-- +-- Name: users FK_2f8d527df0d3acb8aa51945a968; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT "FK_2f8d527df0d3acb8aa51945a968" FOREIGN KEY (addresses_id) REFERENCES public.addresses(id); + + +-- +-- Name: users FK_user_groups; Type: FK CONSTRAINT; Schema: public; Owner: - +-- +ALTER TABLE ONLY public.users + ADD CONSTRAINT "FK_user_groups" FOREIGN KEY (user_groups_id) REFERENCES public.user_groups(id); + + +-- +-- Name: users_have_roles FK_6e768e03083247102b401b74b46; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_have_roles + ADD CONSTRAINT "FK_6e768e03083247102b401b74b46" FOREIGN KEY (role_id) REFERENCES public.roles(id); + + +-- +-- Name: comments FK_980bfefe00ed11685f325d0bd4c; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.comments + ADD CONSTRAINT "FK_980bfefe00ed11685f325d0bd4c" FOREIGN KEY (created_by) REFERENCES public.users(id); + + +-- +-- Name: notes FK_980bfefe00ed11685f325d0bd4c; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.notes + ADD CONSTRAINT "FK_notes" FOREIGN KEY (created_by) REFERENCES public.users(id); + + +-- +-- Name: requests_have_pod_locks FK_c7531fe6bbb926bba3f69fcbb55; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests_have_pod_locks + ADD CONSTRAINT "FK_c7531fe6bbb926bba3f69fcbb55" FOREIGN KEY (pod_id) REFERENCES public.pods(id); + + +-- +-- Name: users_have_roles FK_df6a0246fcd887dd8ffeed2c292; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users_have_roles + ADD CONSTRAINT "FK_df6a0246fcd887dd8ffeed2c292" FOREIGN KEY (user_id) REFERENCES public.users(id); + + +-- +-- Name: requests_have_pod_locks FK_f3729b493fcdb7309cad08837ff; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.requests_have_pod_locks + ADD CONSTRAINT "FK_f3729b493fcdb7309cad08837ff" FOREIGN KEY (request_id) REFERENCES public.requests(id); + + +-- +-- Name: users FK_fba2d8e029689aa8fea98e53c91; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.users + ADD CONSTRAINT "FK_fba2d8e029689aa8fea98e53c91" FOREIGN KEY (manager_id) REFERENCES public.users(id); + + +-- +-- PostgreSQL database dump complete +-- diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts new file mode 100644 index 00000000..3f0c7dab --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts @@ -0,0 +1,69 @@ +import { + PrimaryGeneratedColumn, + OneToOne, + Column, + Entity, + UpdateDateColumn, +} from 'typeorm'; + +import { Users, IUsers } from './index'; + +export type IAddresses = Addresses; + +@Entity('addresses') +export class Addresses { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'varchar', + length: 70, + nullable: true, + default: 'NULL', + }) + public city!: string; + + @Column({ + type: 'varchar', + length: 70, + nullable: true, + default: 'NULL', + }) + public state!: string; + + @Column({ + type: 'varchar', + length: 68, + nullable: true, + default: 'NULL', + }) + public country!: string; + + @Column({ + name: 'array_field', + type: 'varchar', + nullable: true, + default: 'NULL', + array: true, + }) + public arrayField!: string[]; + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @OneToOne(() => Users, (item) => item.addresses) + public user!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts new file mode 100644 index 00000000..c6f3ff8e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts @@ -0,0 +1,57 @@ +import { + PrimaryGeneratedColumn, + Column, + Entity, + JoinColumn, + ManyToOne, + UpdateDateColumn, +} from 'typeorm'; + +export enum CommentKind { + Comment = 'COMMENT', + Message = 'MESSAGE', + Note = 'NOTE', +} + +import { Users, IUsers } from './index'; + +@Entity('comments') +export class Comments { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'text', + nullable: false, + }) + public text!: string; + + @Column({ + type: 'enum', + enum: CommentKind, + nullable: false, + }) + public kind!: CommentKind; + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToOne(() => Users, (item) => item.id) + @JoinColumn({ + name: 'created_by', + }) + public createdBy!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts new file mode 100644 index 00000000..ba42e083 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts @@ -0,0 +1,29 @@ +export * from './users'; +export * from './roles'; +export * from './requests-have-pod-locks'; +export * from './requests'; +export * from './pods'; +export * from './comments'; +export * from './addresses'; +export * from './user-groups'; +export * from './notes'; + +import { Users } from './users'; +import { Roles } from './roles'; +import { Requests } from './requests'; +import { Pods } from './pods'; +import { Comments } from './comments'; +import { Addresses } from './addresses'; +import { UserGroups } from './user-groups'; +import { Notes } from './notes'; + +export const Entities = [ + Users, + Roles, + Requests, + Pods, + Comments, + Addresses, + UserGroups, + Notes, +]; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts new file mode 100644 index 00000000..e8694aca --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts @@ -0,0 +1,44 @@ +import { + PrimaryGeneratedColumn, + Column, + Entity, + JoinColumn, + ManyToOne, + UpdateDateColumn, +} from 'typeorm'; + +import { Users, IUsers } from './index'; + +@Entity('notes') +export class Notes { + @PrimaryGeneratedColumn('uuid') + public id!: string; + + @Column({ + type: 'text', + nullable: false, + }) + public text!: string; + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToOne(() => Users, (item) => item.notes) + @JoinColumn({ + name: 'created_by', + }) + public createdBy!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts new file mode 100644 index 00000000..b5fb898f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts @@ -0,0 +1,45 @@ +import { + Column, + CreateDateColumn, + Entity, + ManyToMany, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; + +import { IRequests, Requests } from './index'; + +export type IPods = Pods; + +@Entity('pods') +export class Pods { + @PrimaryColumn() + public id!: string; + + @Column({ + type: 'varchar', + length: 50, + nullable: false, + unique: true, + }) + public name!: string; + + @CreateDateColumn({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToMany(() => Requests, (item) => item.podLocks) + public lockedRequests!: IRequests[]; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts new file mode 100644 index 00000000..7f1f8125 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts @@ -0,0 +1,91 @@ +import { + AfterLoad, + BeforeInsert, + BeforeRemove, + BeforeUpdate, + Column, + CreateDateColumn, + Entity, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; + +export type IRequestsHavePodLocks = RequestsHavePodLocks; + +@Entity('requests_have_pod_locks') +export class RequestsHavePodLocks { + @PrimaryGeneratedColumn() + public id!: number; + + @AfterLoad() + protected getRequestId() { + this.requestId = this.request_id; + } + + @BeforeInsert() + @BeforeUpdate() + @BeforeRemove() + protected setRequestId() { + if (this.requestId) { + this.request_id = this.requestId; + } + } + + public requestId!: number; + + @AfterLoad() + protected getPodId() { + this.podId = this.pod_id; + } + + @BeforeInsert() + @BeforeUpdate() + @BeforeRemove() + protected setPodId() { + if (this.podId) { + this.pod_id = this.podId; + } + } + + public podId!: number; + + @Column({ + name: 'request_id', + type: 'int', + nullable: false, + }) + protected request_id!: number; + + @Column({ + name: 'pod_id', + type: 'int', + nullable: false, + }) + protected pod_id!: number; + + @CreateDateColumn({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @Column({ + name: 'external_id', + type: 'int', + nullable: true, + unsigned: true, + default: 'NULL', + unique: true, + }) + public externalId!: number; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts new file mode 100644 index 00000000..9bd64266 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts @@ -0,0 +1,48 @@ +import { + PrimaryGeneratedColumn, + Entity, + CreateDateColumn, + UpdateDateColumn, + ManyToMany, + JoinTable, +} from 'typeorm'; + +import { Pods, IPods } from './index'; + +export type IRequests = Requests; + +@Entity('requests') +export class Requests { + @PrimaryGeneratedColumn() + public id!: number; + + @CreateDateColumn({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToMany(() => Pods, (item) => item.lockedRequests) + @JoinTable({ + name: 'requests_have_pod_locks', + inverseJoinColumn: { + referencedColumnName: 'id', + name: 'pod_id', + }, + joinColumn: { + referencedColumnName: 'id', + name: 'request_id', + }, + }) + public podLocks!: IPods[]; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts new file mode 100644 index 00000000..4689628a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts @@ -0,0 +1,58 @@ +import { + PrimaryGeneratedColumn, + Entity, + Column, + ManyToMany, + UpdateDateColumn, +} from 'typeorm'; + +import { Users, IUsers } from './index'; + +@Entity('roles') +export class Roles { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'varchar', + length: 128, + nullable: true, + default: 'NULL', + }) + public name!: string; + + @Column({ + type: 'varchar', + length: 128, + nullable: false, + unique: true, + }) + public key!: string; + + @Column({ + name: 'is_default', + type: 'boolean', + default: 'false', + }) + public isDefault!: boolean; + + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @ManyToMany(() => Users, (item) => item.roles) + public users!: IUsers[]; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts new file mode 100644 index 00000000..a6727416 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts @@ -0,0 +1,20 @@ +import { PrimaryGeneratedColumn, OneToMany, Entity, Column } from 'typeorm'; + +import { IUsers, Users } from './index'; + +@Entity('user_groups') +export class UserGroups { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'varchar', + length: 50, + nullable: false, + unique: true, + }) + public label!: string; + + @OneToMany(() => Users, (item) => item.userGroup) + public users!: IUsers[]; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts new file mode 100644 index 00000000..bdf61878 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts @@ -0,0 +1,133 @@ +import { + PrimaryGeneratedColumn, + ManyToMany, + JoinColumn, + JoinTable, + OneToOne, + OneToMany, + Entity, + Column, + UpdateDateColumn, + ManyToOne, +} from 'typeorm'; + +import { Addresses, Roles, Comments, Notes, UserGroups } from './index'; + +export type IUsers = Users; + +@Entity('users') +export class Users { + @PrimaryGeneratedColumn() + public id!: number; + + @Column({ + type: 'varchar', + length: 100, + nullable: false, + unique: true, + }) + public login!: string; + + @Column({ + name: 'first_name', + type: 'varchar', + length: 100, + nullable: true, + default: 'NULL', + }) + public firstName!: string; + + @Column({ + name: 'test_real', + type: 'real', + array: true, + default: [], + }) + public testReal!: number[]; + + @Column({ + name: 'test_array_null', + type: 'real', + array: true, + nullable: true, + }) + public testArrayNull!: number[] | null; + + @Column({ + name: 'last_name', + type: 'varchar', + length: 100, + nullable: true, + default: 'NULL', + }) + public lastName!: string; + + @Column({ + name: 'is_active', + type: 'boolean', + width: 1, + nullable: true, + default: false, + }) + public isActive!: boolean; + + @Column({ + name: 'created_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public createdAt!: Date; + + @Column({ + name: 'test_date', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public testDate!: Date; + + @UpdateDateColumn({ + name: 'updated_at', + type: 'timestamp', + nullable: true, + default: 'CURRENT_TIMESTAMP', + }) + public updatedAt!: Date; + + @OneToOne(() => Addresses, (item) => item.id) + @JoinColumn({ + name: 'addresses_id', + }) + public addresses!: Addresses; + + @OneToOne(() => Users, (item) => item.id) + @JoinColumn({ + name: 'manager_id', + }) + public manager!: Users; + + @ManyToMany(() => Roles, (item) => item.users) + @JoinTable({ + name: 'users_have_roles', + inverseJoinColumn: { + referencedColumnName: 'id', + name: 'role_id', + }, + joinColumn: { + referencedColumnName: 'id', + name: 'user_id', + }, + }) + public roles!: Roles[]; + + @OneToMany(() => Comments, (item) => item.createdBy) + public comments!: Comments[]; + + @OneToMany(() => Notes, (item) => item.createdBy) + public notes!: Notes[]; + + @ManyToOne(() => UserGroups, (userGroup) => userGroup.id) + @JoinColumn({ name: 'user_groups_id' }) + public userGroup!: UserGroups; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts new file mode 100644 index 00000000..bd6b43f6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts @@ -0,0 +1,94 @@ +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DynamicModule } from '@nestjs/common'; +import { DataType, IMemoryDb, newDb } from 'pg-mem'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +import { + Users, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + UserGroups, + Notes, +} from './entities'; +import { DataSource } from 'typeorm'; + +import { v4 } from 'uuid'; + +export * from './entities'; +export * from './utils'; + +export const entities = [ + Users, + UserGroups, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + Notes, +]; + +export function createAndPullSchemaBase(): IMemoryDb { + const dump = readFileSync(join(__dirname, 'db-for-test'), { + encoding: 'utf8', + }); + const db = newDb({ + autoCreateForeignKeyIndices: true, + }); + + db.public.registerFunction({ + name: 'current_database', + implementation: () => 'test', + }); + + db.public.registerFunction({ + name: 'version', + implementation: () => + 'PostgreSQL 12.5 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.2.1_pre1) 10.2.1 20201203, 64-bit', + }); + + db.registerExtension('uuid-ossp', (schema) => { + schema.registerFunction({ + name: 'uuid_generate_v4', + returns: DataType.uuid, + implementation: v4, + impure: true, + }); + }); + db.public.none(dump); + return db; +} +export function mockDBTestModule(db: IMemoryDb): DynamicModule { + return TypeOrmModule.forRootAsync({ + useFactory() { + return { + type: 'postgres', + // logging: true, + entities: [ + Users, + UserGroups, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + Notes, + ], + }; + }, + async dataSourceFactory(options) { + const dataSource: DataSource = await db.adapters.createTypeormDataSource( + options + ); + + return dataSource; + }, + }); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts new file mode 100644 index 00000000..7cb5aa8b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts @@ -0,0 +1,2 @@ +export * from './pull-data'; +export * from './provider-entities'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts new file mode 100644 index 00000000..4b47577b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts @@ -0,0 +1,69 @@ +import { DataSource, Repository } from 'typeorm'; +import { Provider } from '@nestjs/common'; +import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm'; + +import { + Addresses, + Comments, + Entities, + Notes, + Pods, + Roles, + UserGroups, +} from '../entities'; +import { Users } from '../entities'; +import { DEFAULT_CONNECTION_NAME } from '../../constants'; +import { TestingModule } from '@nestjs/testing'; + +export function providerEntities( + dataSourceToken: ReturnType +): Provider[] { + return Entities.map((entitiy) => { + return { + provide: getRepositoryToken(entitiy, DEFAULT_CONNECTION_NAME), + useFactory(dataSource: DataSource) { + return dataSource.getRepository(entitiy); + }, + inject: [getDataSourceToken()], + }; + }); +} + +export function getRepository(module: TestingModule) { + const userRepository = module.get>( + getRepositoryToken(Users, DEFAULT_CONNECTION_NAME) + ); + + const addressesRepository = module.get>( + getRepositoryToken(Addresses, DEFAULT_CONNECTION_NAME) + ); + + const notesRepository = module.get>( + getRepositoryToken(Notes, DEFAULT_CONNECTION_NAME) + ); + + const commentsRepository = module.get>( + getRepositoryToken(Comments, DEFAULT_CONNECTION_NAME) + ); + const rolesRepository = module.get>( + getRepositoryToken(Roles, DEFAULT_CONNECTION_NAME) + ); + + const userGroupRepository = module.get>( + getRepositoryToken(UserGroups, DEFAULT_CONNECTION_NAME) + ); + + const podsRepository = module.get>( + getRepositoryToken(Pods, DEFAULT_CONNECTION_NAME) + ); + + return { + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + podsRepository, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts new file mode 100644 index 00000000..f28c693e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts @@ -0,0 +1,122 @@ +import { Repository } from 'typeorm'; +import { faker } from '@faker-js/faker'; +import { + Addresses, + CommentKind, + Comments, + Notes, + Roles, + UserGroups, + Users, +} from '../entities'; + +export async function pullAddress(addressRepo: Repository) { + const address = new Addresses(); + address.city = faker.location.city(); + address.country = faker.location.country(); + address.arrayField = [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ]; + address.state = faker.location.state(); + return addressRepo.save(address); +} + +export async function pullComment(commentRepo: Repository) { + const comment = new Comments(); + comment.text = faker.lorem.paragraph(faker.number.int(5)); + comment.kind = CommentKind.Comment; + return commentRepo.save(comment); +} + +export async function pullNote(noteRepo: Repository) { + const note = new Notes(); + note.text = faker.lorem.paragraph(faker.number.int(5)); + return noteRepo.save(note); +} + +export async function pullRole(roleRepo: Repository) { + const role = new Roles(); + role.key = faker.string.alphanumeric(5); + role.name = faker.string.alphanumeric(5); + return roleRepo.save(role); +} + +export async function pullUser(userPero: Repository) { + const user = new Users(); + user.firstName = faker.person.firstName(); + user.lastName = faker.person.lastName(); + user.isActive = faker.datatype.boolean(); + user.login = faker.internet.userName({ + lastName: user.lastName, + firstName: user.firstName, + }); + user.testReal = [faker.number.float({ fractionDigits: 4 })]; + user.testArrayNull = null; + + user.testDate = faker.date.anytime(); + + return userPero.save(user); +} + +export async function pullUserGroup(userGroupRepo: Repository) { + const userGroup = new UserGroups(); + userGroup.label = faker.string.alphanumeric(5); + return userGroupRepo.save(userGroup); +} + +export async function pullAllData( + userPero: Repository, + addressRepo?: Repository, + noteRepo?: Repository, + commentRepo?: Repository, + roleRepo?: Repository, + userGroupRepo?: Repository +) { + const user = await pullUser(userPero); + if (addressRepo) { + user.addresses = await pullAddress(addressRepo); + } + + if (noteRepo) { + user.notes = [ + await pullNote(noteRepo), + await pullNote(noteRepo), + await pullNote(noteRepo), + ]; + } + + if (commentRepo) { + user.comments = [ + await pullComment(commentRepo), + await pullComment(commentRepo), + await pullComment(commentRepo), + await pullComment(commentRepo), + ]; + } + + if (userGroupRepo) { + await pullUserGroup(userGroupRepo); + await pullUserGroup(userGroupRepo); + await pullUserGroup(userGroupRepo); + user.userGroup = await pullUserGroup(userGroupRepo); + } + + if (roleRepo) { + await pullRole(roleRepo); + await pullRole(roleRepo); + await pullRole(roleRepo); + user.roles = [ + await pullRole(roleRepo), + await pullRole(roleRepo), + await pullRole(roleRepo), + ]; + } + + user.manager = await pullUser(userPero); + await pullUser(userPero); + await pullUser(userPero); + await pullUser(userPero); + await userPero.save(user); + return user; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts new file mode 100644 index 00000000..46bf6797 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/atomic-operation.module.ts @@ -0,0 +1,71 @@ +import { AsyncLocalStorage } from 'async_hooks'; +import { + DynamicModule, + Inject, + MiddlewareConsumer, + Module, + NestModule, +} from '@nestjs/common'; +import { DiscoveryModule } from '@nestjs/core'; + +import { OperationController } from './controllers'; +import { ExplorerService, ExecuteService, SwaggerService } from './service'; + +import { + MapControllerEntity, + MapEntityNameToEntity, + ZodInputOperation, + AsyncIterate, +} from './factory'; +import { ResultModuleOptions } from '../../types'; +import { MAP_CONTROLLER_INTERCEPTORS, OPTIONS } from './constants'; + +@Module({}) +export class AtomicOperationModule implements NestModule { + static forRoot( + options: ResultModuleOptions, + entityModules: DynamicModule[], + commonModule: DynamicModule + ): DynamicModule { + return { + module: AtomicOperationModule, + controllers: [OperationController], + providers: [ + ExplorerService, + ExecuteService, + SwaggerService, + AsyncIterate, + MapControllerEntity(options.entities, entityModules), + MapEntityNameToEntity(options.entities), + ZodInputOperation(), + { + provide: MAP_CONTROLLER_INTERCEPTORS, + useValue: new Map(), + }, + { + provide: OPTIONS, + useValue: options.options, + }, + { + provide: AsyncLocalStorage, + useValue: new AsyncLocalStorage(), + }, + ], + imports: [DiscoveryModule, commonModule], + }; + } + @Inject(AsyncLocalStorage) private readonly als!: AsyncLocalStorage; + + configure(consumer: MiddlewareConsumer) { + consumer + .apply((req: any, res: any, next: any) => { + const store = { + req: req, + res: res, + next: next, + }; + this.als.run(store, () => next()); + }) + .forRoutes('*'); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts new file mode 100644 index 00000000..ccace714 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/constants/index.ts @@ -0,0 +1,10 @@ +export const MAP_CONTROLLER_ENTITY = Symbol('MAP_CONTROLLER_ENTITY'); +export const MAP_CONTROLLER_INTERCEPTORS = Symbol( + 'MAP_CONTROLLER_INTERCEPTORS' +); +export const MAP_ENTITY = Symbol('MAP_ENTITY'); +export const ZOD_INPUT_OPERATION = Symbol('ZOD_INPUT_OPERATION'); +export const ASYNC_ITERATOR_FACTORY = Symbol('ASYNC_ITERATOR_FACTORY'); +export const KEY_MAIN_INPUT_SCHEMA = 'atomic:operations'; +export const KEY_MAIN_OUTPUT_SCHEMA = 'atomic:results'; +export const OPTIONS = Symbol('OPTIONS'); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts new file mode 100644 index 00000000..e81188ae --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/index.ts @@ -0,0 +1 @@ +export * from './operation.controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts new file mode 100644 index 00000000..d080815b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts @@ -0,0 +1,220 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DiscoveryModule } from '@nestjs/core'; +import { HttpException } from '@nestjs/common'; +import { Module } from '@nestjs/core/injector/module'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { IMemoryDb } from 'pg-mem'; + +import { OperationController } from './operation.controller'; +import { ExecuteService, ExplorerService } from '../service'; +import { InputArray, Operation } from '../utils'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; +import { + createAndPullSchemaBase, + mockDBTestModule, + providerEntities, + Users, +} from '../../../mock-utils'; + +import { + ASYNC_ITERATOR_FACTORY, + KEY_MAIN_OUTPUT_SCHEMA, + MAP_CONTROLLER_ENTITY, + MAP_ENTITY, + ZOD_INPUT_OPERATION, + MAP_CONTROLLER_INTERCEPTORS, + OPTIONS, +} from '../constants'; + +import { OperationMethode } from '../types'; +import { AsyncLocalStorage } from 'async_hooks'; +import { CURRENT_DATA_SOURCE_TOKEN } from '../../type-orm/constants'; +import { ObjectLiteral } from '../../../types'; +import { RUN_IN_TRANSACTION_FUNCTION } from '../../../constants'; + +describe('OperationController', () => { + let db: IMemoryDb; + let operationController: OperationController; + let explorerService: ExplorerService; + let executeService: ExecuteService; + + beforeEach(async () => { + db = createAndPullSchemaBase(); + const app: TestingModule = await Test.createTestingModule({ + imports: [DiscoveryModule, mockDBTestModule(db)], + controllers: [OperationController], + providers: [ + ...providerEntities(getDataSourceToken()), + { + provide: CURRENT_DATA_SOURCE_TOKEN, + useValue: {}, + }, + ExplorerService, + ExecuteService, + { + provide: MAP_ENTITY, + useValue: {}, + }, + { + provide: RUN_IN_TRANSACTION_FUNCTION, + useValue: {}, + }, + { + provide: MAP_CONTROLLER_ENTITY, + useValue: {}, + }, + { + provide: ASYNC_ITERATOR_FACTORY, + useValue: {}, + }, + { + provide: ZOD_INPUT_OPERATION, + useValue: {}, + }, + { + provide: OPTIONS, + useValue: {}, + }, + { + provide: MAP_CONTROLLER_INTERCEPTORS, + useValue: {}, + }, + { + provide: AsyncLocalStorage, + useValue: new AsyncLocalStorage(), + }, + ], + }).compile(); + + operationController = app.get(OperationController); + explorerService = app.get>(ExplorerService); + executeService = app.get(ExecuteService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('index', () => { + it('should return the result of executeService.run', async () => { + const inputArrayMock: InputArray = [ + { + ref: { + id: '1', + relationship: 'belongs-to', + type: 'TypeA', + }, + op: Operation.add, + }, + ]; + const paramsForExecuteMock = [ + { + module: new (class Module {})() as Module, + params: [1, 'nameRel', { type: 'name', id: '' }], + methodName: 'patchOne', + controller: JsonBaseController, + }, + ]; + + const mockReturnData = { data: { someData: '' } }; + + const getControllerByEntityNameSpy = jest + .spyOn(explorerService, 'getControllerByEntityName') + .mockReturnValue(paramsForExecuteMock[0].controller); + const getMethodNameByParamSpy = jest + .spyOn(explorerService, 'getMethodNameByParam') + .mockReturnValue( + paramsForExecuteMock[0].methodName as OperationMethode + ); + const getModulesByControllerSpy = jest + .spyOn(explorerService, 'getParamsForMethod') + .mockReturnValue( + paramsForExecuteMock[0].params as Parameters< + JsonBaseController['deleteOne'] + > + ); + const getParamsForMethodSpy = jest + .spyOn(explorerService, 'getModulesByController') + .mockReturnValue(paramsForExecuteMock[0].module); + const runSpy = jest + .spyOn(executeService, 'run') + .mockResolvedValue([mockReturnData] as never); + + expect(await operationController.index(inputArrayMock)).toEqual({ + [KEY_MAIN_OUTPUT_SCHEMA]: [mockReturnData], + }); + + expect(getControllerByEntityNameSpy).toHaveBeenCalledWith('TypeA'); + expect(getMethodNameByParamSpy).toHaveBeenCalledWith( + inputArrayMock[0].op, + inputArrayMock[0].ref.id, + inputArrayMock[0].ref.relationship + ); + expect(getModulesByControllerSpy).toHaveBeenCalledWith( + paramsForExecuteMock[0].methodName, + { op: inputArrayMock[0].op, ref: inputArrayMock[0].ref } + ); + expect(getParamsForMethodSpy).toHaveBeenCalledWith( + paramsForExecuteMock[0].controller + ); + + expect(runSpy).toHaveBeenCalledWith(paramsForExecuteMock, []); + }); + + it('should throw NotFoundException when type does not exist', async () => { + const inputArrayMock: any[] = [ + { + ref: { + id: '1', + relationship: 'belongs-to', + type: 'TypeA', + }, + op: Operation.add, + }, + ]; + + jest + .spyOn(explorerService, 'getControllerByEntityName') + .mockImplementationOnce(() => { + throw new HttpException('Resource does not exist', 404); + }); + expect.assertions(1); + try { + await operationController.index(inputArrayMock as InputArray); + } catch (e) { + expect(e).toBeInstanceOf(HttpException); + } + }); + + it('should throw MethodNotAllowedException when operation not allowed', async () => { + const inputArrayMock = [ + { + ref: { + id: '1', + relationship: 'belongs-to', + type: 'TypeA', + }, + op: Operation.add, + }, + ]; + + jest + .spyOn(explorerService, 'getControllerByEntityName') + .mockReturnValue(Promise.resolve({}) as any); + + jest + .spyOn(explorerService, 'getMethodNameByParam') + .mockImplementationOnce(() => { + throw new HttpException('Operation not allowed', 405); + }); + + expect.assertions(1); + try { + await operationController.index(inputArrayMock as InputArray); + } catch (e) { + expect(e).toBeInstanceOf(HttpException); + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts new file mode 100644 index 00000000..7042e289 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.ts @@ -0,0 +1,104 @@ +import { + Body, + Controller, + Inject, + MethodNotAllowedException, + NotFoundException, + Post, + Type, +} from '@nestjs/common'; +import { Module } from '@nestjs/core/injector/module'; + +import { InputArray } from '../utils'; +import { InputOperationPipe } from '../pipes/input-operation.pipe'; +import { ExecuteService, ExplorerService } from '../service'; +import { KEY_MAIN_INPUT_SCHEMA, KEY_MAIN_OUTPUT_SCHEMA } from '../constants'; +import { OperationMethode, ParamsForExecute } from '../types'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; +import { ObjectLiteral as Entity, ValidateQueryError } from '../../../types'; + +@Controller('/') +export class OperationController { + @Inject(ExplorerService) private readonly explorerService!: ExplorerService; + @Inject(ExecuteService) private readonly executeService!: ExecuteService; + + @Post('') + async index(@Body(InputOperationPipe) inputOperationData: InputArray) { + const paramForCall: ParamsForExecute[] = []; + let i = 0; + for (const dataInput of inputOperationData) { + const { + ref: { relationship, id, type }, + op, + } = dataInput; + + let controller: Type>; + let methodName: OperationMethode; + let module: Module; + try { + controller = this.explorerService.getControllerByEntityName(type); + } catch (e) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${type}' does not exist`, + path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'ref', 'type'], + }; + throw new NotFoundException([error]); + } + try { + methodName = this.explorerService.getMethodNameByParam( + op, + id, + relationship + ); + } catch (e) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Operation '${op}' not allowed`, + path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'op'], + }; + throw new MethodNotAllowedException([error]); + } + + const params = this.explorerService.getParamsForMethod( + methodName, + dataInput + ); + + try { + module = this.explorerService.getModulesByController(controller); + } catch (e) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${type}' does not exist`, + path: [KEY_MAIN_INPUT_SCHEMA, `${i}`, 'ref', 'type'], + }; + throw new NotFoundException([error]); + } + + paramForCall.push({ + controller, + methodName, + params, + module, + }); + + i++; + } + const tmpIds: (string | number)[] = []; + for (const item of inputOperationData) { + if (item.op !== 'add') continue; + if (!item.ref.tmpId) continue; + tmpIds.push(item.ref.tmpId); + } + + const result = await this.executeService.run(paramForCall, tmpIds); + + return { + [KEY_MAIN_OUTPUT_SCHEMA]: result.map((i) => ({ + data: i.data, + ...(i.meta ? { meta: i.meta } : {}), + })), + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts new file mode 100644 index 00000000..fd9f15b1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/async-iterator.ts @@ -0,0 +1,75 @@ +import { Provider } from '@nestjs/common'; +import { ASYNC_ITERATOR_FACTORY } from '../constants'; + +type ParamsInput = R extends (...arg: infer P) => any ? P : never; + +type ParamsReturn = R extends (...arg: any) => infer P + ? P extends Promise + ? T extends [infer K, ...any] + ? K + : T + : P + : never; + +export type IterateFactory< + R extends (...arg: any) => any = (...arg: any) => any +> = { + createIterator: ( + iterateObject: ParamsInput, + callback: R + ) => { + [Symbol.asyncIterator](): GeneralAsyncIterator< + R, + ParamsInput, + ParamsReturn + >; + }; +}; + +class GeneralAsyncIterator< + R extends (...arg: any[]) => any, + T = ParamsInput, + TReturn = ParamsReturn +> implements AsyncIterator +{ + private counter = 0; + private maxLimit!: number; + + constructor(private iterateObject: T[], private callback: R) { + if (!Array.isArray(iterateObject)) { + throw new Error('Expected iterateObject to be an array'); + } + this.maxLimit = iterateObject.length; + } + + async next(): Promise> { + const items = !Array.isArray(this.iterateObject[this.counter]) + ? [this.iterateObject[this.counter]] + : (this.iterateObject[this.counter] as T[]); + this.counter++; + + if (this.counter <= this.maxLimit) { + return this.callback(...items).then((r: TReturn) => ({ + done: false, + value: r, + })); + } else { + return Promise.resolve({ done: true, value: {} as TReturn }); + } + } +} + +export const AsyncIterate: Provider = { + provide: ASYNC_ITERATOR_FACTORY, + useFactory: () => ({ + createIterator any>( + iterateObject: ParamsInput, + callback: R + ) { + return { + [Symbol.asyncIterator]: () => + new GeneralAsyncIterator(iterateObject, callback), + }; + }, + }), +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts new file mode 100644 index 00000000..0dded8b9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/index.ts @@ -0,0 +1,4 @@ +export * from './zod-input-operation'; +export * from './map-controller-entity'; +export * from './map-entity-name-to-entity'; +export * from './async-iterator'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts new file mode 100644 index 00000000..64d23a17 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-controller-entity.ts @@ -0,0 +1,25 @@ +import { DynamicModule, ValueProvider } from '@nestjs/common'; +import { Type } from '@nestjs/common/interfaces/type.interface'; +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; +import { MapController } from '../types'; +import { MAP_CONTROLLER_ENTITY } from '../constants'; + +export function MapControllerEntity( + entities: EntityClassOrSchema[], + entityModules: DynamicModule[] +): ValueProvider { + const mapController = entities.reduce((acum, entity, index) => { + const entityModule = entityModules[index]; + if (entityModule.controllers) { + const controller = entityModule.controllers.at(0); + if (controller) acum.set(entity, controller); + } + + return acum; + }, new Map>()); + + return { + provide: MAP_CONTROLLER_ENTITY, + useValue: mapController, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts new file mode 100644 index 00000000..50a1207a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts @@ -0,0 +1,19 @@ +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; +import { ValueProvider } from '@nestjs/common'; +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; +import { MapEntity } from '../types'; +import { MAP_ENTITY } from '../constants'; +import { getEntityName } from '../../mixin/helper'; +import { AnyEntity, EntityTarget } from '../../../types'; + +export function MapEntityNameToEntity( + entities: EntityClassOrSchema[] +): ValueProvider { + return { + provide: MAP_ENTITY, + useValue: entities.reduce( + (acum, item) => acum.set(camelToKebab(getEntityName(item)), item), + new Map>() + ), + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts new file mode 100644 index 00000000..620b2b52 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts @@ -0,0 +1,22 @@ +import { FactoryProvider } from '@nestjs/common'; +import { MAP_CONTROLLER_ENTITY, ZOD_INPUT_OPERATION } from '../constants'; +import { MapController } from '../types'; +import { zodInputOperation, ZodInputOperation } from '../utils'; +import { FIELD_FOR_ENTITY } from '../../../constants'; +import { GetFieldForEntity } from '../../mixin/types'; +import { ObjectLiteral } from '../../../types'; + +export function ZodInputOperation(): FactoryProvider< + ZodInputOperation +> { + return { + provide: ZOD_INPUT_OPERATION, + useFactory( + mapController: MapController, + getField: GetFieldForEntity + ) { + return zodInputOperation(mapController, getField); + }, + inject: [MAP_CONTROLLER_ENTITY, FIELD_FOR_ENTITY], + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts new file mode 100644 index 00000000..81ce0c2a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/index.ts @@ -0,0 +1 @@ +export * from './atomic-operation.module'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts new file mode 100644 index 00000000..643405e1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.spec.ts @@ -0,0 +1,75 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ZodError } from 'zod'; +import { + InternalServerErrorException, + BadRequestException, +} from '@nestjs/common'; + +import { InputOperationPipe } from './input-operation.pipe'; + +import { KEY_MAIN_INPUT_SCHEMA, ZOD_INPUT_OPERATION } from '../constants'; +import { ZodInputOperation } from '../utils'; + +describe('PatchInputPipe', () => { + let patchInputPipe: InputOperationPipe; + let zodInputOperation: ZodInputOperation; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ZOD_INPUT_OPERATION, + useValue: { + parse() {}, + }, + }, + InputOperationPipe, + ], + }).compile(); + + patchInputPipe = module.get(InputOperationPipe); + zodInputOperation = module.get(ZOD_INPUT_OPERATION); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('It should be ok', () => { + const data = { + some: 'data', + }; + const check = { + [KEY_MAIN_INPUT_SCHEMA]: data, + }; + jest + .spyOn(zodInputOperation, 'parse') + .mockImplementationOnce(() => check as any); + expect(patchInputPipe.transform(check)).toEqual(data); + }); + + it('Should be not ok', () => { + jest.spyOn(zodInputOperation, 'parse').mockImplementationOnce(() => { + throw new ZodError([]); + }); + expect.assertions(1); + try { + patchInputPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + it('Should be 500', () => { + jest.spyOn(zodInputOperation, 'parse').mockImplementationOnce(() => { + throw new Error('Error mock'); + }); + expect.assertions(1); + + try { + patchInputPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(InternalServerErrorException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts new file mode 100644 index 00000000..a920101f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/pipes/input-operation.pipe.ts @@ -0,0 +1,32 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { errorMap } from 'zod-validation-error'; +import { ZodError } from 'zod'; +import { JSONValue } from '../../mixin/types'; +import { InputArray, ZodInputOperation } from '../utils'; +import { KEY_MAIN_INPUT_SCHEMA, ZOD_INPUT_OPERATION } from '../constants'; + +export class InputOperationPipe + implements PipeTransform +{ + @Inject(ZOD_INPUT_OPERATION) + private zodInputOperation!: ZodInputOperation; + + transform(value: JSONValue): InputArray { + try { + return this.zodInputOperation.parse(value, { + errorMap: errorMap, + })[KEY_MAIN_INPUT_SCHEMA]; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts new file mode 100644 index 00000000..45723122 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.spec.ts @@ -0,0 +1,609 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ModuleRef } from '@nestjs/core'; +import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; +import { ExecuteService, isZodError } from './execute.service'; +import { IterateFactory } from '../factory'; +import { + ASYNC_ITERATOR_FACTORY, + KEY_MAIN_INPUT_SCHEMA, + MAP_CONTROLLER_INTERCEPTORS, + OPTIONS, +} from '../constants'; + +import { + HttpException, + NotFoundException, + ParseIntPipe, + ParseBoolPipe, +} from '@nestjs/common'; +import { ParamsForExecute } from '../types'; +import { AsyncLocalStorage } from 'async_hooks'; +import { RUN_IN_TRANSACTION_FUNCTION } from '../../../constants'; + +describe('ExecuteService', () => { + let service: ExecuteService; + let runInTransaction: jest.Mock; + let moduleRef: ModuleRef; + let asyncIteratorFactory: IterateFactory; + const mapControllerInterceptors = new Map(); + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ExecuteService, + { + provide: RUN_IN_TRANSACTION_FUNCTION, + useValue: jest.fn(), + }, + { + provide: ModuleRef, + useValue: { + get() {}, + }, + }, + { + provide: OPTIONS, + useValue: {}, + }, + { + provide: ASYNC_ITERATOR_FACTORY, + useValue: { + createIterator: () => {}, + }, + }, + { + provide: MAP_CONTROLLER_INTERCEPTORS, + useValue: mapControllerInterceptors, + }, + { + provide: AsyncLocalStorage, + useValue: new AsyncLocalStorage(), + }, + ], + }).compile(); + + service = module.get(ExecuteService); + runInTransaction = module.get(RUN_IN_TRANSACTION_FUNCTION); + moduleRef = module.get(ModuleRef); + asyncIteratorFactory = module.get(ASYNC_ITERATOR_FACTORY); + mapControllerInterceptors.clear(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('run', () => { + it('should throw NotFoundException if controller not found', async () => { + const params = [ + { + controller: { name: 'NonExistentController' }, + module: { controllers: new Map() }, + }, + ] as ParamsForExecute[]; + + runInTransaction.mockImplementationOnce((args: () => {}) => args()); + + jest.spyOn(service as any, 'executeOperations').mockImplementation(() => { + throw new NotFoundException(); + }); + + await expect(service.run(params, [])).rejects.toThrow(NotFoundException); + + expect(runInTransaction).toHaveBeenCalled(); + }); + + it('should return an empty array if no operations are executed', async () => { + const params: ParamsForExecute[] = []; + + runInTransaction.mockImplementationOnce((args: () => {}) => args()); + jest.spyOn(service as any, 'executeOperations').mockReturnValue([]); + + const result = await service.run(params, []); + expect(result).toEqual([]); + expect(runInTransaction).toHaveBeenCalled(); + }); + }); + + describe('executeOperations', () => { + it('should correctly execute operations', async () => { + const params: ParamsForExecute[] = [ + { + controller: { name: 'TestController' }, + methodName: 'someMethod', + }, + ] as unknown as ParamsForExecute[]; + const callback = jest.fn().mockReturnValue({ value: 'test' }); + const mapController = { + someMethod: callback, + }; + jest + .spyOn(service as any, 'getControllerInstance') + .mockReturnValue(mapController); + + mapControllerInterceptors.set(mapController, new Map([[callback, []]])); + let callCount = 0; + jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ + [Symbol.asyncIterator]: () => + ({ + next: () => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ value: 'test', done: false }); + } else { + return Promise.resolve({ value: undefined, done: true }); + } + }, + } as any), + }); + + const result = await (service as any).executeOperations(params, []); + + expect(result).toEqual([{ value: 'test' }]); + }); + + it('should return an empty array if controller method does not return an object', async () => { + const params: ParamsForExecute[] = [ + { + controller: { name: 'TestController' }, + methodName: 'someMethod', + }, + ] as unknown as ParamsForExecute[]; + + const callback = jest.fn().mockReturnValue('not an object'); + const mapController = { + someMethod: callback, + }; + jest + .spyOn(service as any, 'getControllerInstance') + .mockReturnValue(mapController); + + mapControllerInterceptors.set(mapController, new Map([[callback, []]])); + + let callCount = 0; + jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ + [Symbol.asyncIterator]: () => + ({ + next: () => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ value: 'not an object', done: false }); + } else { + return Promise.resolve({ value: undefined, done: true }); + } + }, + } as any), + }); + + const result = await (service as any).executeOperations(params, []); + + expect(result).toEqual([]); + }); + + it('should call processException if an exception is thrown during execution', async () => { + const params: ParamsForExecute[] = [ + { + controller: { name: 'TestController' }, + methodName: 'someMethod', + }, + ] as unknown as ParamsForExecute[]; + + const callback = jest.fn().mockImplementation(() => { + throw new HttpException('Test exception', 400); + }); + const mapController = { + someMethod: callback, + }; + jest + .spyOn(service as any, 'getControllerInstance') + .mockReturnValue(mapController); + + mapControllerInterceptors.set(mapController, new Map([[callback, []]])); + + let callCount = 0; + jest.spyOn(asyncIteratorFactory, 'createIterator').mockReturnValue({ + [Symbol.asyncIterator]: () => + ({ + next: () => { + callCount++; + if (callCount === 1) { + return Promise.resolve({ value: 'test', done: false }); + } else { + return Promise.resolve({ value: undefined, done: true }); + } + }, + } as any), + }); + + const processExceptionSpy = jest.spyOn( + service as any, + 'processException' + ); + + await expect((service as any).executeOperations(params)).rejects.toThrow( + HttpException + ); + + expect(processExceptionSpy).toHaveBeenCalled(); + }); + }); + + describe('getControllerInstance', () => { + it('should throw NotFoundException if controller not found', () => { + const params: ParamsForExecute = { + controller: { name: 'NonExistentController' }, + module: { controllers: new Map() }, + } as unknown as ParamsForExecute; + + expect(() => (service as any).getControllerInstance(params)).toThrow( + NotFoundException + ); + }); + + it('should return controller instance if controller is found', () => { + const controllerInstance = { + someMethod: jest.fn().mockReturnValue('test'), + }; + function TestController() {} + const params: ParamsForExecute = { + controller: TestController, + methodName: 'someMethod', + module: { + controllers: new Map([ + [TestController, { instance: controllerInstance }], + ]), + }, + } as unknown as ParamsForExecute; + + const result = (service as any).getControllerInstance(params); + + expect(result).toBe(controllerInstance); + }); + }); + + describe('processException', () => { + it('should rethrow HttpException with modified response if ZodError is thrown', () => { + const exception = new HttpException( + { + message: [{ path: ['test'] }], + }, + 400 + ); + + try { + (service as any).processException(exception, 1); + } catch (e) { + if (e instanceof HttpException) { + const response = e.getResponse(); + if (isZodError(response)) { + expect(response['message'][0]['path']).toEqual([ + KEY_MAIN_INPUT_SCHEMA, + '1', + 'test', + ]); + } else { + fail('Exception response is not a ZodError'); + } + } else { + fail('Caught exception is not a HttpException'); + } + } + }); + + it('should rethrow the original exception if it is not a HttpException', () => { + const exception = new Error('Test exception'); + + expect(() => (service as any).processException(exception, 1)).toThrow( + Error + ); + }); + }); + + describe('runOneOperation', () => { + it('should correctly run operation', async () => { + const controllerInstance = { + someMethod: jest.fn().mockReturnValue('test'), + }; + function TestController() {} + const pipes = [ + { index: 0, pipes: [] }, + { index: 1, pipes: [] }, + ]; + const params: ParamsForExecute = { + controller: TestController, + methodName: 'someMethod', + module: { + controllers: new Map([ + [TestController, { instance: controllerInstance }], + ]), + }, + params: ['param1', 'param2'], + } as unknown as ParamsForExecute; + + Reflect.defineMetadata( + ROUTE_ARGS_METADATA, + { 0: pipes[0], 1: pipes[1] }, + TestController, + 'someMethod' + ); + + const runPipesSpy = jest + .spyOn(service as any, 'runPipes') + .mockImplementation((param) => `modified_${param}`); + + await (service as any).runOneOperation(params); + + expect(runPipesSpy).toHaveBeenCalledWith( + 'param1', + params.module, + pipes[0].pipes + ); + expect(runPipesSpy).toHaveBeenCalledWith( + 'param2', + params.module, + pipes[1].pipes + ); + }); + + it('should not call runPipes if metadata is empty', async () => { + const controllerInstance = { + someMethod: jest.fn().mockReturnValue('test'), + }; + function TestController() {} + const params: ParamsForExecute = { + controller: TestController, + methodName: 'someMethod', + module: { + controllers: new Map([ + [TestController, { instance: controllerInstance }], + ]), + }, + params: ['param1', 'param2'], + } as unknown as ParamsForExecute; + + Reflect.defineMetadata( + ROUTE_ARGS_METADATA, + {}, + TestController, + 'someMethod' + ); + + const runPipesSpy = jest + .spyOn(service as any, 'runPipes') + .mockImplementation((param) => `modified_${param}`); + + await (service as any).runOneOperation(params); + + expect(runPipesSpy).not.toHaveBeenCalled(); + }); + }); + + describe('runPipes', () => { + it('should correctly run pipes', async () => { + const value = 'test'; + const pipes = [new ParseBoolPipe(), new ParseIntPipe()]; + const module = {} as any; + + jest + .spyOn(pipes[0], 'transform') + // @ts-ignore + .mockImplementation((val) => `validated_${val}`); + + jest + .spyOn(pipes[1], 'transform') + // @ts-ignore + .mockImplementation((val) => `parsed_${val}`); + const getPipeInstanceSpy = jest + .spyOn(service as any, 'getPipeInstance') + .mockImplementation((pipe) => + pipe instanceof ParseBoolPipe ? pipes[0] : pipes[1] + ); + + const result = await (service as any).runPipes(value, module, [ + pipes[0], + pipes[1], + ]); + + expect(result).toBe('parsed_validated_test'); + expect(getPipeInstanceSpy).toHaveBeenCalledTimes(2); + expect(getPipeInstanceSpy).toHaveBeenNthCalledWith(1, pipes[0], module); + expect(getPipeInstanceSpy).toHaveBeenNthCalledWith(2, pipes[1], module); + }); + + it('should not call getPipeInstance if pipes array is empty', async () => { + const value = 'test'; + const module = {} as any; + + const getPipeInstanceSpy = jest.spyOn(service as any, 'getPipeInstance'); + + const result = await (service as any).runPipes(value, module, []); + + expect(result).toBe('test'); + expect(getPipeInstanceSpy).not.toHaveBeenCalled(); + }); + }); + + describe('getPipeInstance', () => { + it('should return pipe instance from module if it exists', () => { + const pipe = new ParseBoolPipe(); + const module = { + getProviderByKey: jest.fn().mockReturnValue({ instance: pipe }), + } as any; + + const result = (service as any).getPipeInstance(ParseBoolPipe, module); + + expect(result).toBe(pipe); + expect(module.getProviderByKey).toHaveBeenCalledWith(ParseBoolPipe); + }); + + it('should return pipe instance from moduleRef if it does not exist in module', () => { + const pipe = new ParseBoolPipe(); + const module = { + getProviderByKey: jest.fn().mockReturnValue(null), + } as any; + jest.spyOn(service['moduleRef'], 'get').mockReturnValue(pipe); + + const result = (service as any).getPipeInstance(ParseBoolPipe, module); + + expect(result).toBe(pipe); + expect(service['moduleRef'].get).toHaveBeenCalledWith(ParseBoolPipe, { + strict: false, + }); + }); + }); + + describe('ExecuteService - replaceTmpIds', () => { + let service: ExecuteService; + + beforeEach(() => { + service = new ExecuteService(); + }); + + it('should be return id first input params of array is undefined', () => { + const inputParams = [] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is string', () => { + const inputParams = ['string'] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is number', () => { + const inputParams = [1] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + it('should be return id first input params of array is array', () => { + const inputParams = [[]] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is object', () => { + const inputParams = [{}] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be return id first input params of array is object with undefined of relationships', () => { + const inputParams = [{ relationships: undefined }] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be not replace of relation with object', () => { + const inputParams = [ + { + relationships: { + addresses: { data: { id: '1234', type: 'addresses' } }, + }, + }, + ] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be not replace of relation with array of object', () => { + const inputParams = [ + { + relationships: { + addresses: { + data: [ + { id: '1234', type: 'addresses' }, + { id: '1235', type: 'addresses' }, + ], + }, + }, + }, + ] as any; + const tmpIdsMap = {}; + + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(inputParams); + }); + + it('should be replace of relation with object', () => { + const inputParams = [ + { + relationships: { + addresses: { data: { id: '1234', type: 'addresses' } }, + }, + }, + ] as any; + const newId = '4321'; + const tmpIdsMap = { '1234': newId }; + + const checkResult = [ + { + ...inputParams[0], + relationships: { + ...inputParams[0].relationships, + addresses: { + ...inputParams[0].relationships.addresses, + data: { + ...inputParams[0].relationships.addresses.data, + id: newId, + }, + }, + }, + }, + ]; + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(checkResult); + }); + + it('should be replace of relation with array of object', () => { + const inputParams = [ + { + relationships: { + addresses: { + data: [ + { id: '12345', type: 'addresses' }, + { id: '1234', type: 'addresses' }, + ], + }, + }, + }, + ] as any; + const newId = '4321'; + const tmpIdsMap = { '1234': newId }; + + const checkResult = [ + { + ...inputParams[0], + relationships: { + ...inputParams[0].relationships, + addresses: { + ...inputParams[0].relationships.addresses, + data: [ + inputParams[0].relationships.addresses.data[0], + { + ...inputParams[0].relationships.addresses.data[1], + id: newId, + }, + ], + }, + }, + }, + ]; + const result = service.replaceTmpIds(inputParams, tmpIdsMap); + expect(result).toEqual(checkResult); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts new file mode 100644 index 00000000..0afc0f95 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts @@ -0,0 +1,338 @@ +import { + HttpException, + NotFoundException, + Inject, + Injectable, + PipeTransform, + Type, +} from '@nestjs/common'; +import { + INTERCEPTORS_METADATA, + ROUTE_ARGS_METADATA, +} from '@nestjs/common/constants'; +import { Module } from '@nestjs/core/injector/module'; +import { ArgumentMetadata } from '@nestjs/common/interfaces/features/pipe-transform.interface'; +import { ApplicationConfig, ModuleRef, NestContainer } from '@nestjs/core'; +import { + ObjectTyped, + ResourceObject, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; +import { + InterceptorsConsumer, + InterceptorsContextCreator, +} from '@nestjs/core/interceptors'; +import { Controller } from '@nestjs/common/interfaces'; +import { lastValueFrom } from 'rxjs'; +import { AsyncLocalStorage } from 'async_hooks'; + +import { MapControllerInterceptor, ParamsForExecute } from '../types'; +import { + ASYNC_ITERATOR_FACTORY, + KEY_MAIN_INPUT_SCHEMA, + MAP_CONTROLLER_INTERCEPTORS, +} from '../constants'; +import { IterateFactory } from '../factory'; +import { TypeFromType } from '../../mixin/types'; +import { RunInTransaction, ValidateQueryError } from '../../../types'; +import { RUN_IN_TRANSACTION_FUNCTION } from '../../../constants'; + +export function isZodError( + param: string | unknown +): param is { message: ValidateQueryError[] } { + return ( + param instanceof Object && + 'message' in param && + Array.isArray(param.message) && + 'path' in param.message[0] + ); +} + +@Injectable() +export class ExecuteService { + // @Inject(CURRENT_DATA_SOURCE_TOKEN) private readonly dataSource!: DataSource; + @Inject(ModuleRef) private readonly moduleRef!: ModuleRef & { + container: NestContainer; + applicationConfig: ApplicationConfig; + _moduleKey: string; + }; + @Inject(ASYNC_ITERATOR_FACTORY) private asyncIteratorFactory!: IterateFactory< + ExecuteService['runOneOperation'] + >; + @Inject(RUN_IN_TRANSACTION_FUNCTION) + private runInTransaction!: RunInTransaction< + () => ReturnType + >; + @Inject(MAP_CONTROLLER_INTERCEPTORS) + private mapControllerInterceptor!: MapControllerInterceptor; + + @Inject(AsyncLocalStorage) private asyncLocalStorage!: AsyncLocalStorage; + + private _interceptorsContextCreator!: InterceptorsContextCreator; + + get interceptorsContextCreator() { + if (!this._interceptorsContextCreator) { + this._interceptorsContextCreator = new InterceptorsContextCreator( + this.moduleRef.container, + this.moduleRef.applicationConfig + ); + } + + return this._interceptorsContextCreator; + } + + private interceptorsConsumer = new InterceptorsConsumer(); + + async run(params: ParamsForExecute[], tmpIds: (string | number)[]) { + return this.runInTransaction(() => this.executeOperations(params, tmpIds)); + } + + protected async executeOperations( + params: ParamsForExecute[], + tmpIds: (string | number)[] = [] + ) { + const iterateParams = this.asyncIteratorFactory.createIterator( + params as Parameters, + this.runOneOperation.bind(this) as ExecuteService['runOneOperation'] + ); + + const resultArray: Array< + ResourceObject | ResourceObjectRelationships + > = []; + let i = 0; + const tmpIdsMap: Record = {}; + try { + for await (const item of iterateParams) { + const currentParams = params[i]; + const controller = this.getControllerInstance(currentParams); + const methodName = + currentParams.methodName as (typeof currentParams)['methodName']; + + const paramsForExecute = item as unknown as ParamsForExecute['params']; + + const itemReplace = this.replaceTmpIds(paramsForExecute, tmpIdsMap); + const body = itemReplace.at(-1); + const currentTmpId = tmpIds[i]; + + if (methodName === 'postOne' && currentTmpId && body) { + if (typeof body === 'object' && 'attributes' in body) { + body['id'] = `${currentTmpId}`; + itemReplace[itemReplace.length - 1]; + } + } + + const interceptors = this.getInterceptorsArray( + controller, + controller[methodName], + currentParams.module + ); + + const result$: any = await this.interceptorsConsumer.intercept( + interceptors, + [ + ...Object.values(this.asyncLocalStorage.getStore() || {}), + itemReplace, + ], + controller, + // @ts-ignore + controller[methodName], + // @ts-ignore + async () => controller[methodName](...itemReplace) + ); + + const result = + interceptors.length === 0 + ? await result$ + : await lastValueFrom(result$); + + if (tmpIds[i] && result && !Array.isArray(result.data) && result.data) { + tmpIdsMap[tmpIds[i]] = result.data.id; + } + + if (result instanceof Object) { + resultArray.push(result); + } + i++; + } + } catch (e) { + this.processException(e, i); + } + return resultArray; + } + + private getInterceptorsArray( + controller: Controller, + callback: (...arg: any) => any, + module: ParamsForExecute['module'] + ) { + let controllerFromMap = this.mapControllerInterceptor.get(controller); + + if (!controllerFromMap) { + controllerFromMap = new Map(); + this.mapControllerInterceptor.set(controller, controllerFromMap); + } + + const interceptorsFromMap = controllerFromMap.get(callback); + + if (interceptorsFromMap) { + return interceptorsFromMap; + } + + const interceptorsForController = this.interceptorsContextCreator.create( + controller, + callback, + module.token + ); + + const interceptorsForMethode = new Set( + Reflect.getMetadata(INTERCEPTORS_METADATA, callback) || [] + ); + + const resultInterceptors = interceptorsForController.filter((i) => + interceptorsForMethode.has(i.constructor) + ); + controllerFromMap.set(callback, resultInterceptors); + return resultInterceptors; + } + + replaceTmpIds( + inputParams: T, + tmpIdsMap: Record + ): T { + const bodyInput = inputParams.at(-1); + if (!bodyInput) { + return inputParams; + } + if (typeof bodyInput === 'string') { + return inputParams; + } + if (typeof bodyInput === 'number') { + return inputParams; + } + + if (Array.isArray(bodyInput)) { + return inputParams; + } + + if (!(typeof bodyInput === 'object' && 'relationships' in bodyInput)) { + return inputParams; + } + + const { relationships } = bodyInput; + + if (!relationships) { + return inputParams; + } + + bodyInput.relationships = ObjectTyped.entries(relationships).reduce( + (acum, [name, val]) => { + if (!val) throw new Error('Va; undefined'); + const { data } = val; + if (Array.isArray(data)) { + acum[name] = { + data: data.map((i) => { + if (i === null) return i; + return { + ...i, + id: tmpIdsMap[i['id']] ? `${tmpIdsMap[i['id']]}` : i['id'], + }; + }), + }; + } else { + if (!data) { + acum[name] = val; + } else { + data['id'] = tmpIdsMap[data['id']] + ? `${tmpIdsMap[data['id']]}` + : data['id']; + acum[name] = { + data, + }; + } + } + return acum; + }, + { ...relationships } + ); + + inputParams[inputParams.length - 1] = bodyInput; + return inputParams; + } + + private getControllerInstance(params: ParamsForExecute) { + const controllerClass = params.controller; + const controllerInstanceWrapper = + params.module.controllers.get(controllerClass); + + if (!controllerInstanceWrapper) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['type'], + message: `Controller "${controllerClass.name}" not found`, + }; + throw new NotFoundException([error]); + } + + return controllerInstanceWrapper.instance as TypeFromType< + ParamsForExecute['controller'] + >; + } + + private processException(e: any, i: number) { + if (e instanceof HttpException) { + const response = e.getResponse(); + if (isZodError(response)) { + response['message'] = response['message'].map((m: any) => { + m['path'] = [KEY_MAIN_INPUT_SCHEMA, `${i}`, ...m['path']]; + return m; + }); + } + throw new HttpException(response, e.getStatus()); + } + throw e; + } + + private async runOneOperation( + paramForExecute: ParamsForExecute + ): Promise { + const { params, controller, methodName, module } = paramForExecute; + const pramsPipe = Object.values( + Reflect.getMetadata(ROUTE_ARGS_METADATA, controller, methodName) + ) as unknown as { + index: number; + pipes: Type[]; + }[]; + const resultParams = new Array(params.length); + for (const { pipes, index } of pramsPipe) { + resultParams[index] = await this.runPipes(params[index], module, pipes); + } + return resultParams as unknown as ParamsForExecute['params']; + } + + private async runPipes( + initialParams: unknown, + module: Module, + pipes: Type[] + ) { + let modifiedParams = initialParams; + for (const pipe of pipes) { + const pipeInstance = this.getPipeInstance(pipe, module); + modifiedParams = await pipeInstance.transform( + modifiedParams, + {} as ArgumentMetadata + ); + } + return modifiedParams; + } + + private getPipeInstance( + pipe: Type, + module: Module + ): PipeTransform { + const instanceWrapper = module.getProviderByKey(pipe); + if (!instanceWrapper) { + return this.moduleRef.get(pipe, { strict: false }); + } + return instanceWrapper.instance; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts new file mode 100644 index 00000000..ec654d2d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.spec.ts @@ -0,0 +1,97 @@ +import { Test } from '@nestjs/testing'; +import { ModulesContainer } from '@nestjs/core'; +import { + MAP_ENTITY, + MAP_CONTROLLER_ENTITY, + OPTIONS, + MAP_CONTROLLER_INTERCEPTORS, +} from '../constants'; +import { Operation } from '../utils'; +import { ExplorerService } from './explorer.service'; + +describe('ExplorerService', () => { + let service: ExplorerService; + class EntityName {} + class ControllerName {} + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + providers: [ + ExplorerService, + { + provide: ModulesContainer, + useValue: new Map([ + [ + 'TestModule', + { + controllers: new Map([['ControllerName', ControllerName]]), + }, + ], + ]), + }, + { + provide: MAP_ENTITY, + useValue: new Map([['EntityName', EntityName]]), + }, + { + provide: MAP_CONTROLLER_ENTITY, + useValue: new Map([[EntityName, ControllerName]]), + }, + { + provide: MAP_CONTROLLER_INTERCEPTORS, + useValue: new Map(), + }, + { + provide: OPTIONS, + useValue: {}, + }, + ], + }).compile(); + + service = moduleRef.get(ExplorerService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getControllerByEntityName()', () => { + it('should return the correct controller for a given entity name', () => { + expect(service.getControllerByEntityName('EntityName')).toBeDefined(); + }); + }); + + describe('getMethodNameByParam()', () => { + it('should return the correct method name for given parameters', () => { + expect(service.getMethodNameByParam(Operation.add, 'id', 'rel')).toBe( + 'postRelationship' + ); + }); + }); + + describe('getParamsForMethod()', () => { + it('should return the correct parameters for a given method name', () => { + const data = { + ref: { + id: '1', + relationship: 'belongs-to', + type: 'TypeA', + }, + op: Operation.add, + data: {}, + }; + expect(service.getParamsForMethod('patchRelationship', data)).toEqual([ + data.ref.id, + data.ref.relationship, + { data: data.data }, + ]); + }); + }); + + describe('getModulesByController()', () => { + it('should return the correct module for a given controller', () => { + expect( + service.getModulesByController(ControllerName as any) + ).toBeDefined(); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts new file mode 100644 index 00000000..1ad13223 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts @@ -0,0 +1,109 @@ +import { Inject, Injectable, Type } from '@nestjs/common'; +import { Module } from '@nestjs/core/injector/module'; +import { ModulesContainer } from '@nestjs/core'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { MAP_CONTROLLER_ENTITY, MAP_ENTITY } from '../constants'; +import { MapController, MapEntity, OperationMethode } from '../types'; +import { ObjectLiteral as Entity } from '../../../types'; +import { InputArray, Operation } from '../utils'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, +} from '../../mixin/zod'; + +@Injectable() +export class ExplorerService { + @Inject(ModulesContainer) + private readonly modulesContainer!: ModulesContainer; + + @Inject(MAP_ENTITY) private readonly mapEntity!: MapEntity; + @Inject(MAP_CONTROLLER_ENTITY) private readonly mapController!: MapController; + + private mapModuleByController = new Map< + Type>, + Module + >(); + + getControllerByEntityName(entityName: string): Type> { + const entity = this.mapEntity.get(entityName); + if (!entity) { + throw new Error(); + } + + const controller = this.mapController.get(entity); + if (!controller) { + throw new Error(); + } + + return controller; + } + + getMethodNameByParam( + operation: Operation, + id?: string, + rel?: string + ): OperationMethode { + switch (operation) { + case Operation.add: + return id ? 'postRelationship' : 'postOne'; + case Operation.remove: + return rel ? 'deleteRelationship' : 'deleteOne'; + case Operation.update: + return rel ? 'patchRelationship' : 'patchOne'; + default: + throw new Error(); + } + } + + getParamsForMethod( + methodName: OperationMethode, + data: InputArray[number] + ): Parameters[typeof methodName]> { + const { op, ref, ...other } = data; + switch (methodName) { + case 'postOne': + return [other as PostData]; + case 'patchOne': + return [ref.id as string, other as PatchData]; + case 'deleteOne': + return [ref.id as string]; + case 'deleteRelationship': + return [ + ref.id as string, + ref.relationship as EntityRelation, + other as PostRelationshipData, + ]; + case 'patchRelationship': + return [ + ref.id as string, + ref.relationship as EntityRelation, + other as PatchRelationshipData, + ]; + case 'postRelationship': + return [ + ref.id as string, + ref.relationship as EntityRelation, + other as PostRelationshipData, + ]; + } + } + + getModulesByController(controllers: Type>): Module { + const module = this.mapModuleByController.get(controllers); + if (module) { + return module; + } + + const findModule = [...this.modulesContainer.values()].find((i) => + [...i.controllers.values()].find((c) => c.name === controllers.name) + ); + if (findModule) { + return findModule; + } + + throw new Error(); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts new file mode 100644 index 00000000..aaf75a95 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/index.ts @@ -0,0 +1,3 @@ +export * from './explorer.service'; +export * from './execute.service'; +export * from './swagger.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts new file mode 100644 index 00000000..cf263440 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/swagger.service.ts @@ -0,0 +1,97 @@ +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { generateSchema } from '@anatine/zod-openapi'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; + +import { OperationController } from '../controllers'; +import { ZodInputOperation } from '../utils'; +import { ZOD_INPUT_OPERATION } from '../constants'; + +@Injectable() +export class SwaggerService implements OnModuleInit { + @Inject(ModuleRef) private readonly moduleRef!: ModuleRef; + @Inject(ZOD_INPUT_OPERATION) + private typeZodInputOperation!: ZodInputOperation; + + private initSwagger() { + const operationControllerInst = this.moduleRef.get(OperationController); + if (!operationControllerInst) + throw new Error('OperationController not found'); + const controller = operationControllerInst.constructor.prototype; + const descriptor = Reflect.getOwnPropertyDescriptor(controller, 'index'); + if (!descriptor) + throw new Error(`Descriptor for controller OperationController is empty`); + + ApiTags('Atomic operation')(operationControllerInst.constructor); + ApiOperation({ + summary: `Atomic operation for several entity"`, + operationId: `atomic_operation`, + })(controller, 'index', descriptor); + + ApiBody({ + description: `Json api schema for new atomic operatiom`, + schema: generateSchema(this.typeZodInputOperation) as + | SchemaObject + | ReferenceObject, + required: true, + examples: { + allField: { + summary: 'Examples several operation', + description: 'Examples several operation', + value: { + ['atomic:operations']: [ + { + op: 'add', + ref: { + type: 'users', + }, + data: 'EntityPostOne', + }, + { + op: 'update', + ref: { + type: 'users', + id: '1', + }, + data: 'EntityPatchOne', + }, + { + op: 'remove', + ref: { + type: 'users', + id: '1', + }, + }, + { + op: 'add', + ref: { + type: 'users', + id: '1', + relationship: 'EntityRelationName', + }, + data: 'UsersPostRelationship', + }, + { + op: 'update', + ref: { + type: 'users', + id: '1', + relationship: 'EntityRelationName', + }, + data: 'UsersDeleteRelationship', + }, + ], + }, + }, + }, + })(controller, 'index', descriptor); + } + + onModuleInit(): void { + this.initSwagger(); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts new file mode 100644 index 00000000..57fe5ae5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/types/index.ts @@ -0,0 +1,33 @@ +import { NestInterceptor, Type } from '@nestjs/common'; +import { Module } from '@nestjs/core/injector/module'; +import { Controller } from '@nestjs/common/interfaces'; +import { EntityTarget, ObjectLiteral } from '../../../types'; +import { JsonBaseController } from '../../mixin/controller/json-base.controller'; + +export type MapControllerInterceptor = Map< + Controller, + Map<(...arg: any) => any, NestInterceptor[]> +>; +export type MapController = Map< + EntityTarget, + Type +>; +export type MapEntity = Map< + string, + EntityTarget +>; + +export type OperationMethode = keyof Omit< + { [k in keyof JsonBaseController]: string }, + 'getAll' | 'getOne' | 'getRelationship' +>; + +export type ParamsForExecute< + E extends ObjectLiteral = ObjectLiteral, + O extends OperationMethode = OperationMethode +> = { + methodName: O; + controller: Type>; + params: Parameters[O]>; + module: Module; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts new file mode 100644 index 00000000..405fa85d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/index.ts @@ -0,0 +1 @@ +export * from './zod/zod-helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts new file mode 100644 index 00000000..9419ce57 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts @@ -0,0 +1,591 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { z, ZodError } from 'zod'; +import { + Operation, + ZodAdd, + zodAdd, + zodInputOperation, + ZodInputOperation, + zodOperationRel, + ZodOperationRel, + zodRemove, + ZodRemove, + zodUpdate, + ZodUpdate, +} from './zod-helper'; +import { Users } from '../../../../mock-utils'; +import { FIELD_FOR_ENTITY } from '../../../../constants'; +import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; +import { MapController } from '../../types'; +import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; +import { GetFieldForEntity, TupleOfEntityRelation } from '../../../mixin/types'; + +describe('ZodHelperSpec', () => { + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + describe('zodAdd', () => { + it('should be correct', () => { + const user = 'user'; + const schema = zodAdd(user); + const check: z.infer> = { + op: Operation.add, + ref: { + type: user, + }, + data: {}, + }; + const check1: z.infer> = { + op: Operation.add, + ref: { + type: user, + }, + data: [{}], + }; + const check2: z.infer> = { + op: Operation.add, + ref: { + type: user, + }, + data: null, + }; + const check3: z.infer> = { + op: Operation.add, + ref: { + type: user, + }, + data: null, + }; + const checkArray = [check, check1, check2, check3]; + for (const item of checkArray) { + const result = schema.parse(item); + expect(result.op).toBe(Operation.add); + expect(result.ref.type).toBe(user); + expect(result).toHaveProperty('data'); + } + }); + it('should be not correct', () => { + const schema = zodAdd('user'); + const check = { + op: Operation.add, + ref: { + type: 'user', + }, + data: {}, + sdfsf: {}, + }; + const check1 = { + op: Operation.add, + ref: { + type: 'user', + }, + }; + const check2 = { + op: Operation.add, + ref: { + type: 'user', + sdsdf: 'ssdfdsf', + }, + data: {}, + }; + const check3 = { + op: Operation.add, + ref: { + type12: 'user', + }, + data: {}, + }; + const check4 = { + op: Operation.add, + ref: { + type: 'sdfsdf', + }, + data: {}, + }; + const check5 = { + op: 'sdfsdf', + ref: { + type: 'user', + }, + data: {}, + }; + + const checkArray = [check, check1, check2, check3, check4, check5]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + describe('zodUpdate', () => { + it('should be correct', () => { + const user = 'user'; + const schema = zodUpdate(user); + const check: z.infer> = { + op: Operation.update, + ref: { + type: 'user', + id: '1', + }, + data: {}, + }; + const checkArray = [check]; + for (const item of checkArray) { + const result = schema.parse(item); + expect(result.op).toBe(Operation.update); + expect(result.ref.type).toBe(user); + expect(result).toHaveProperty('data'); + } + }); + it('should be not correct', () => { + const schema = zodUpdate('user'); + const check = { + op: Operation.update, + ref: { + type: 'user', + id: '12', + }, + data: {}, + sdfsf: {}, + }; + const check1 = { + op: Operation.update, + ref: { + type: 'user', + id: '12', + }, + }; + const check2 = { + op: Operation.update, + ref: { + type: 'user', + id: '12', + sdsdf: 'ssdfdsf', + }, + data: {}, + }; + const check3 = { + op: Operation.update, + ref: { + type12: 'user', + id: '12', + }, + data: {}, + }; + const check4 = { + op: Operation.update, + ref: { + type: 'sdfsdf', + id: '12', + }, + data: {}, + }; + const check5 = { + op: 'sdfsdf', + ref: { + type: 'user', + id: '12', + }, + data: {}, + }; + const check6 = { + op: Operation.update, + ref: { + type: 'user', + }, + data: {}, + }; + + const checkArray = [ + check, + check1, + check2, + check3, + check4, + check5, + check6, + ]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + describe('zodRemove', () => { + it('should be correct', () => { + const user = 'user'; + const schema = zodRemove(user); + const check: z.infer> = { + op: Operation.remove, + ref: { + type: 'user', + id: '1', + }, + }; + const checkArray = [check]; + for (const item of checkArray) { + const result = schema.parse(item); + expect(result.op).toBe(Operation.remove); + expect(result.ref.type).toBe(user); + expect(result).not.toHaveProperty('data'); + } + }); + + it('should be not correct', () => { + const schema = zodRemove('user'); + const check = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + }, + sdfsf: {}, + }; + const check1 = { + op: Operation.remove, + ref: { + type: 'user', + idsdf: '12', + }, + }; + const check2 = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + sdsdf: 'ssdfdsf', + }, + }; + const check3 = { + op: Operation.remove, + ref: { + type12: 'user', + id: '12', + }, + }; + const check4 = { + op: Operation.remove, + ref: { + type: 'sdfsdf', + id: '12', + }, + }; + const check5 = { + op: 'sdfsdf', + ref: { + type: 'user', + id: '12', + }, + }; + const check6 = { + op: Operation.remove, + ref: { + type: 'user', + }, + }; + + const checkArray = [ + check, + check1, + check2, + check3, + check4, + check5, + check6, + ]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + describe('zodOperationRel', () => { + it('should be correct', () => { + const user = 'user'; + const rel = [ + 'address', + 'notes', + ] as unknown as TupleOfEntityRelation; + const schema = zodOperationRel( + user, + rel, + Operation.remove + ); + const check: z.infer> = { + op: Operation.remove, + ref: { + type: 'user', + id: '1', + relationship: 'notes', + }, + data: { + id: 1, + type: 'notes', + }, + }; + const checkArray = [check]; + for (const item of checkArray) { + const result = schema.parse(item); + expect(result.op).toBe(Operation.remove); + expect(result.ref.type).toBe(user); + expect(result).toHaveProperty('data'); + expect(result['data']).toEqual(check.data); + } + }); + it('should be not correct', () => { + const user = 'user'; + const rel = [ + 'address', + 'notes', + ] as unknown as TupleOfEntityRelation; + const schema = zodOperationRel( + user, + rel, + Operation.remove + ); + + const check = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + relationship: 'notes', + }, + data: {}, + sdfsf: {}, + }; + const check1 = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + relationship: 'notes', + sdfsdf: 'sdfsdf', + }, + data: {}, + }; + const check2 = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + relationship1: 'notes', + }, + data: {}, + }; + const check3 = { + op: Operation.remove, + ref: { + type12: 'user', + id: '12', + relationship: 'notes', + }, + data: {}, + }; + const check4 = { + op: Operation.remove, + ref: { + type: 'sdfsdf', + id: '12', + relationship: 'notes', + }, + data: {}, + }; + const check5 = { + op: 'sdfsdf', + ref: { + type: 'user', + id: '12', + relationship: 'notes', + }, + data: {}, + }; + const check6 = { + op: Operation.remove, + ref: { + type: 'user', + id: '12', + relationship: 'notes1', + }, + data: {}, + }; + + const checkArray = [ + check, + check1, + check2, + check3, + check4, + check5, + check6, + ]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + describe('zodInputOperation', () => { + let getField: GetFieldForEntity; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: FIELD_FOR_ENTITY, + useValue: () => ({ + relations: [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ], + }), + }, + ], + }).compile(); + getField = module.get>(FIELD_FOR_ENTITY); + }); + + it('should be correct', () => { + const mapController: MapController = new Map([ + [Users as any, JsonBaseController], + ]); + const schema = zodInputOperation(mapController, getField); + const check: z.infer> = { + [KEY_MAIN_INPUT_SCHEMA]: [ + { + data: {}, + op: Operation.update, + ref: { + type: 'users', + relationship: 'manager', + id: '1', + }, + }, + { + data: {}, + op: Operation.update, + ref: { + type: 'users', + id: '1', + }, + }, + { + data: {}, + op: Operation.add, + ref: { + type: 'users', + }, + }, + { + op: Operation.remove, + ref: { + type: 'users', + id: '1', + }, + }, + ], + }; + expect(schema.parse(check)).toEqual(check); + }); + + it('incorrect input main data', () => { + const mapController: MapController = new Map([ + [Users as any, JsonBaseController], + ]); + const schema = zodInputOperation(mapController, getField); + const check = {}; + const check1 = { + ssdf: 'sdfsdf', + }; + const check2 = { + [KEY_MAIN_INPUT_SCHEMA]: null, + }; + const check3 = { + [KEY_MAIN_INPUT_SCHEMA]: '', + }; + const check4 = { + [KEY_MAIN_INPUT_SCHEMA]: {}, + }; + const check5 = { + [KEY_MAIN_INPUT_SCHEMA]: [], + }; + const checkArray = [check, check1, check2, check3, check4, check5]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + + it('should be incorrect methode not allow', () => { + class Test extends JsonBaseController { + override deleteOne(id: string | number): Promise { + return super.deleteOne(id); + } + } + const mapController: MapController = new Map([ + [Users as any, Test], + ]); + const schema = zodInputOperation(mapController, getField); + const check: z.infer> = { + [KEY_MAIN_INPUT_SCHEMA]: [ + { + data: {}, + op: Operation.update, + ref: { + type: 'users', + relationship: 'manager', + id: '1', + }, + }, + ], + }; + const check1: z.infer> = { + [KEY_MAIN_INPUT_SCHEMA]: [ + { + data: {}, + op: Operation.remove, + ref: { + type: 'users1', + relationship: 'manager', + id: '1', + }, + }, + ], + }; + const checkArray = [check, check1]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts new file mode 100644 index 00000000..3b2734c7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts @@ -0,0 +1,176 @@ +import { + z, + ZodArray, + ZodLiteral, + ZodNumber, + ZodObject, + ZodOptional, + ZodString, + ZodType, + ZodUnion, +} from 'zod'; +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; + +import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; +import { MapController } from '../../types'; +import { GetFieldForEntity, TupleOfEntityRelation } from '../../../mixin/types'; +import { getEntityName } from '../../../mixin/helper'; +import { ObjectLiteral } from '../../../../types'; + +export enum Operation { + add = 'add', + update = 'update', + remove = 'remove', +} + +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; + +const jsonSchema: ZodType = z.lazy(() => + z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)]) +); + +const zodGeneralData = jsonSchema.nullable(); +type ZodGeneral = typeof zodGeneralData; + +export type ZodAdd = ReturnType>; +export const zodAdd = (type: T) => + z + .object({ + op: z.literal(Operation.add), + ref: z + .object({ + type: z.literal(type), + tmpId: z.union([z.number(), z.string()]).optional(), + }) + .strict(), + data: zodGeneralData, + }) + .strict(); + +export type ZodUpdate = ReturnType>; +export const zodUpdate = (type: T) => + z + .object({ + op: z.literal(Operation.update), + ref: z + .object({ + type: z.literal(type), + id: z.string(), + }) + .strict(), + data: zodGeneralData, + }) + .strict(); +export type ZodRemove = ReturnType>; +export const zodRemove = (type: T) => + z + .object({ + op: z.literal(Operation.remove), + ref: z + .object({ + type: z.literal(type), + id: z.string(), + }) + .strict(), + }) + .strict(); + +export type ZodOperationRel< + E extends ObjectLiteral, + O extends Operation +> = ReturnType>; + +export const zodOperationRel = ( + type: string, + rel: TupleOfEntityRelation, + typeOperation: O +) => { + const literalArray = rel.map((i) => z.literal(i)) as [ + ZodLiteral, + ZodLiteral, + ...ZodLiteral[] + ]; + + return z + .object({ + op: z.literal(typeOperation), + ref: z + .object({ + type: z.literal(type), + id: z.string(), + relationship: z.union(literalArray), + }) + .strict(), + data: zodGeneralData, + }) + .strict(); +}; +export type ZodInputArray = ZodArray< + ZodObject<{ + op: ZodLiteral; + ref: ZodObject<{ + type: ZodString; + id: ZodOptional; + relationship: ZodOptional; + tmpId: ZodOptional>; + }>; + data: ZodOptional; + }>, + 'atleastone' +>; + +export type ZodInputOperation = + ReturnType>; +export type InputOperation = z.infer< + ZodInputOperation +>; + +export type InputArray = z.infer; + +export function zodInputOperation( + mapController: MapController, + getField: GetFieldForEntity +) { + const array = [] as unknown as [ + ZodAdd, + ZodUpdate, + ZodRemove, + ZodOperationRel, + ZodOperationRel, + ZodOperationRel + ]; + for (const [entity, controller] of mapController.entries()) { + const typeName = camelToKebab(getEntityName(entity)); + const { relations } = getField(entity); + + const hasOwnProperty = (props: string) => + Object.prototype.hasOwnProperty.call(controller.prototype, props); + + if (hasOwnProperty('postOne')) { + array.push(zodAdd(typeName)); + } + if (hasOwnProperty('patchOne')) { + array.push(zodUpdate(typeName)); + } + if (hasOwnProperty('deleteOne')) { + array.push(zodRemove(typeName)); + } + if (hasOwnProperty('postRelationship')) { + array.push(zodOperationRel(typeName, relations, Operation.add)); + } + if (hasOwnProperty('deleteRelationship')) { + array.push(zodOperationRel(typeName, relations, Operation.remove)); + } + if (hasOwnProperty('patchRelationship')) { + array.push(zodOperationRel(typeName, relations, Operation.update)); + } + } + + return z + .object({ + [KEY_MAIN_INPUT_SCHEMA]: z.array(z.union(array)).nonempty(), + }) + .strict(); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/index.ts new file mode 100644 index 00000000..06b783b6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/index.ts @@ -0,0 +1,3 @@ +export * from './type-orm'; +export * from './micro-orm'; +export * from './atomic-operation'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts new file mode 100644 index 00000000..4b7a1069 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts @@ -0,0 +1,2 @@ +export * from './micro-orm.module'; +export * from './type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts new file mode 100644 index 00000000..c2b3095f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts @@ -0,0 +1,14 @@ +import { NestProvider, ResultModuleOptions, ObjectLiteral } from '../../types'; +import { DynamicModule } from '@nestjs/common'; + +export class MicroOrmModule { + static forRoot(options: ResultModuleOptions): DynamicModule { + return { + module: MicroOrmModule, + }; + } + + static getUtilProviders(entity: ObjectLiteral): NestProvider { + return []; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts new file mode 100644 index 00000000..6a557459 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts @@ -0,0 +1,3 @@ +export type MicroOrmParam = { + // testParam: boolean; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts new file mode 100644 index 00000000..4c256991 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.spec.ts @@ -0,0 +1,9 @@ +import { Bindings, excludeMethod } from './bindings'; + +describe('bindings', () => { + it('excludeMethod', () => { + expect(excludeMethod(['patchRelationship'])).toEqual( + Object.keys(Bindings).filter((i) => i !== 'patchRelationship') + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts new file mode 100644 index 00000000..45eb5708 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts @@ -0,0 +1,202 @@ +import { Body, Param, Query, RequestMethod } from '@nestjs/common'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +import { BindingsConfig, MethodName } from '../types'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { PARAMS_RELATION_NAME, PARAMS_RESOURCE_ID } from '../../../constants'; + +import { + queryInputMixin, + queryMixin, + queryFiledInIncludeMixin, + queryCheckSelectFieldMixin, + idPipeMixin, + checkItemEntityPipeMixin, + postInputPipeMixin, + patchInputPipeMixin, + parseRelationshipNamePipeMixin, + postRelationshipPipeMixin, + patchRelationshipPipeMixin, +} from '../pipe'; + +const Bindings: BindingsConfig = { + getAll: { + method: RequestMethod.GET, + name: 'getAll', + path: '/', + implementation: JsonBaseController.prototype.getAll, + parameters: [ + { + decorator: Query, + mixins: [ + queryInputMixin, + queryMixin, + queryFiledInIncludeMixin, + queryCheckSelectFieldMixin, + ], + }, + ], + }, + getOne: { + method: RequestMethod.GET, + name: 'getOne', + path: `:${PARAMS_RESOURCE_ID}`, + implementation: JsonBaseController.prototype.getOne, + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + decorator: Query, + mixins: [ + queryInputMixin, + queryMixin, + queryFiledInIncludeMixin, + queryCheckSelectFieldMixin, + ], + }, + ], + }, + deleteOne: { + method: RequestMethod.DELETE, + name: 'deleteOne', + path: `:${PARAMS_RESOURCE_ID}`, + implementation: JsonBaseController.prototype.deleteOne, + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + ], + }, + postOne: { + method: RequestMethod.POST, + name: 'postOne', + path: '/', + implementation: JsonBaseController.prototype.postOne, + parameters: [ + { + decorator: Body, + mixins: [postInputPipeMixin], + }, + ], + }, + patchOne: { + method: RequestMethod.PATCH, + name: 'patchOne', + path: `:${PARAMS_RESOURCE_ID}`, + implementation: JsonBaseController.prototype.patchOne, + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + decorator: Body, + mixins: [patchInputPipeMixin], + }, + ], + }, + getRelationship: { + path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, + name: 'getRelationship', + method: RequestMethod.GET, + implementation: JsonBaseController.prototype.getRelationship, + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + property: PARAMS_RELATION_NAME, + decorator: Param, + mixins: [parseRelationshipNamePipeMixin], + }, + ], + }, + postRelationship: { + path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, + name: 'postRelationship', + method: RequestMethod.POST, + implementation: JsonBaseController.prototype['postRelationship'], + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + property: PARAMS_RELATION_NAME, + decorator: Param, + mixins: [parseRelationshipNamePipeMixin], + }, + { + decorator: Body, + mixins: [postRelationshipPipeMixin], + }, + ], + }, + deleteRelationship: { + path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, + name: 'deleteRelationship', + method: RequestMethod.DELETE, + implementation: JsonBaseController.prototype['deleteRelationship'], + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + property: PARAMS_RELATION_NAME, + decorator: Param, + mixins: [parseRelationshipNamePipeMixin], + }, + { + decorator: Body, + mixins: [postRelationshipPipeMixin], + }, + ], + }, + patchRelationship: { + path: `:${PARAMS_RESOURCE_ID}/relationships/:${PARAMS_RELATION_NAME}`, + name: 'patchRelationship', + method: RequestMethod.PATCH, + implementation: JsonBaseController.prototype['patchRelationship'], + parameters: [ + { + property: PARAMS_RESOURCE_ID, + decorator: Param, + mixins: [idPipeMixin, checkItemEntityPipeMixin], + }, + { + property: PARAMS_RELATION_NAME, + decorator: Param, + mixins: [parseRelationshipNamePipeMixin], + }, + { + decorator: Body, + mixins: [patchRelationshipPipeMixin], + }, + ], + }, +}; + +export { Bindings }; + +export function excludeMethod( + names: Array> +): Array { + const tmpObject = names.reduce( + (acum, key) => ((acum[key] = true), acum), + {} as Record, boolean> + ); + return ObjectTyped.keys(Bindings).filter( + (method) => !tmpObject[method] + ) as Array; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts new file mode 100644 index 00000000..28aa9ce7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts @@ -0,0 +1,79 @@ +import { + EntityRelation, + ResourceObject, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; + +import { ORM_SERVICE_PROPS } from '../../../constants'; +import { MethodName } from '../types'; +import { ObjectLiteral } from '../../../types'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../zod'; +import { OrmService } from '../types'; + +type RequestMethodeObject = { + [K in MethodName]: OrmService[K]; +}; + +export class JsonBaseController + implements RequestMethodeObject +{ + private [ORM_SERVICE_PROPS]!: OrmService; + + getOne(id: string | number, query: QueryOne): Promise> { + return this[ORM_SERVICE_PROPS].getOne(id, query); + } + getAll(query: Query): Promise> { + return this[ORM_SERVICE_PROPS].getAll(query); + } + deleteOne(id: string | number): Promise { + return this[ORM_SERVICE_PROPS].deleteOne(id); + } + + patchOne( + id: string | number, + inputData: PatchData + ): Promise> { + return this[ORM_SERVICE_PROPS].patchOne(id, inputData); + } + + postOne(inputData: PostData): Promise> { + return this[ORM_SERVICE_PROPS].postOne(inputData); + } + + getRelationship>( + id: string | number, + relName: Rel + ): Promise> { + return this[ORM_SERVICE_PROPS].getRelationship(id, relName); + } + postRelationship>( + id: string | number, + relName: Rel, + input: PostRelationshipData + ): Promise> { + return this[ORM_SERVICE_PROPS].postRelationship(id, relName, input); + } + + deleteRelationship>( + id: string | number, + relName: Rel, + input: PostRelationshipData + ): Promise { + return this[ORM_SERVICE_PROPS].deleteRelationship(id, relName, input); + } + + patchRelationship>( + id: string | number, + relName: Rel, + input: PatchRelationshipData + ): Promise> { + return this[ORM_SERVICE_PROPS].patchRelationship(id, relName, input); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts new file mode 100644 index 00000000..2036a50b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/index.ts @@ -0,0 +1,2 @@ +export * from './json-api/json-api.decorator'; +export * from './inject-service/inject-service.decorator'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts new file mode 100644 index 00000000..e1485972 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.spec.ts @@ -0,0 +1,30 @@ +import { + PROPERTY_DEPS_METADATA, + SELF_DECLARED_DEPS_METADATA, +} from '@nestjs/common/constants'; +import 'reflect-metadata'; + +import { InjectService } from './inject-service.decorator'; +import { ORM_SERVICE } from '../../../../constants'; + +describe('InjectServiceDecorator', () => { + it('should save property key', () => { + class SomeClass { + @InjectService() protected property: any; + constructor(@InjectService() protected test: any) {} + } + + const properties = Reflect.getMetadata(PROPERTY_DEPS_METADATA, SomeClass); + const properties1 = Reflect.getMetadata( + SELF_DECLARED_DEPS_METADATA, + SomeClass + ); + expect( + properties.find((item: any) => item.type === ORM_SERVICE) + ).toBeDefined(); + + expect( + properties1.find((item: any) => item.param === ORM_SERVICE) + ).toBeDefined(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts new file mode 100644 index 00000000..ab1c5157 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/inject-service/inject-service.decorator.ts @@ -0,0 +1,7 @@ +import { Inject } from '@nestjs/common'; + +import { ORM_SERVICE } from '../../../../constants'; + +export function InjectService(): PropertyDecorator & ParameterDecorator { + return Inject(ORM_SERVICE); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts new file mode 100644 index 00000000..fd246eb9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.spec.ts @@ -0,0 +1,69 @@ +import 'reflect-metadata'; + +import { + JSON_API_DECORATOR_ENTITY, + JSON_API_DECORATOR_OPTIONS, +} from '../../../../constants'; +import { JsonApi } from './json-api.decorator'; + +import { excludeMethod, Bindings } from '../../config/bindings'; +import { DecoratorOptions } from '../../types'; + +describe('InjectServiceDecorator', () => { + it('should save entity in class', () => { + const testedEntity = class SomeEntity {}; + + @JsonApi(testedEntity) + class SomeClass {} + + const data = Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, SomeClass); + expect(data).toBe(testedEntity); + }); + + it('should save options in class', () => { + const testedEntity = class SomeEntity {}; + const apiOptions: DecoratorOptions = { + allowMethod: ['getAll', 'deleteRelationship'], + }; + + @JsonApi(testedEntity, apiOptions) + class SomeClass {} + + const data = Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, SomeClass); + expect(data).toEqual(apiOptions); + }); + + it('should save options in class using helpFunction', () => { + const testedEntity = class SomeEntity {}; + const example = ['getAll', 'deleteRelationship']; + const apiOptions: DecoratorOptions = { + allowMethod: excludeMethod(example as any), + }; + + @JsonApi(testedEntity, apiOptions) + class SomeClass {} + + const data: DecoratorOptions = Reflect.getMetadata( + JSON_API_DECORATOR_OPTIONS, + SomeClass + ); + expect(data).toEqual(apiOptions); + expect(data.allowMethod).toEqual( + Object.keys(Bindings).filter((k) => !example.includes(k)) + ); + }); + + it('should save options in class and correctly set overrideRoute', () => { + const testedEntity = class SomeEntity {}; + const apiOptions: DecoratorOptions = { + allowMethod: ['getAll', 'deleteRelationship'], + overrideRoute: '123', + }; + + @JsonApi(testedEntity, apiOptions) + class SomeClass {} + + const data = Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, SomeClass); + expect(data).toEqual(apiOptions); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts new file mode 100644 index 00000000..c73f8594 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/decorators/json-api/json-api.decorator.ts @@ -0,0 +1,19 @@ +import { + JSON_API_DECORATOR_ENTITY, + JSON_API_DECORATOR_OPTIONS, +} from '../../../../constants'; +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { DecoratorOptions } from '../../types'; + +export function JsonApi( + entity: EntityClass, + options?: DecoratorOptions +): ClassDecorator { + return (target): typeof target => { + Reflect.defineMetadata(JSON_API_DECORATOR_ENTITY, entity, target); + if (options) { + Reflect.defineMetadata(JSON_API_DECORATOR_OPTIONS, options, target); + } + return target; + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts new file mode 100644 index 00000000..0f09495c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/index.ts @@ -0,0 +1 @@ +export * from './zod-validate.factory'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts new file mode 100644 index 00000000..ca42707e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts @@ -0,0 +1,160 @@ +import { FactoryProvider, ValueProvider } from '@nestjs/common'; + +import { + PARAMS_FOR_ZOD_SCHEMA, + ZOD_INPUT_QUERY_SCHEMA, + ZOD_QUERY_SCHEMA, + ZOD_POST_SCHEMA, + ZOD_PATCH_SCHEMA, + ZOD_POST_RELATIONSHIP_SCHEMA, + ZOD_PATCH_RELATIONSHIP_SCHEMA, +} from '../../../constants'; + +import { + zodInputQuery, + ZodInputQuery, + zodQuery, + ZodQuery, + ZodPost, + zodPost, + zodPatch, + ZodPatch, + zodPostRelationship, + ZodPostRelationship, + zodPatchRelationship, + ZodPatchRelationship, +} from '../zod'; +import { ObjectLiteral } from '../../../types'; +import { EntityProps, ZodParams } from '../types'; + +export function ZodInputQuerySchema(): FactoryProvider< + ZodInputQuery +> { + return { + provide: ZOD_INPUT_QUERY_SCHEMA, + inject: [ + { + token: PARAMS_FOR_ZOD_SCHEMA, + optional: false, + }, + ], + useFactory: (zodParams: ZodParams>) => { + const { entityFieldsStructure, entityRelationStructure } = zodParams; + return zodInputQuery(entityFieldsStructure, entityRelationStructure); + }, + }; +} + +export function ZodQuerySchema(): FactoryProvider< + ZodQuery +> { + return { + provide: ZOD_QUERY_SCHEMA, + inject: [ + { + token: PARAMS_FOR_ZOD_SCHEMA, + optional: false, + }, + ], + useFactory: (zodParams: ZodParams>) => { + const { + entityFieldsStructure, + entityRelationStructure, + propsType, + propsArray, + } = zodParams; + return zodQuery( + entityFieldsStructure, + entityRelationStructure, + propsArray, + propsType + ); + }, + }; +} + +export function ZodPostSchema< + E extends ObjectLiteral, + I extends string +>(): FactoryProvider> { + return { + provide: ZOD_POST_SCHEMA, + inject: [ + { + token: PARAMS_FOR_ZOD_SCHEMA, + optional: false, + }, + ], + useFactory: (zodParams: ZodParams, I>) => { + const { + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType, + } = zodParams; + return zodPost( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + }, + }; +} + +export function ZodPatchSchema< + E extends ObjectLiteral, + I extends string +>(): FactoryProvider> { + return { + provide: ZOD_PATCH_SCHEMA, + inject: [ + { + token: PARAMS_FOR_ZOD_SCHEMA, + optional: false, + }, + ], + useFactory: (zodParams: ZodParams, I>) => { + const { + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType, + } = zodParams; + return zodPatch( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + }, + }; +} + +export const ZodInputPostRelationshipSchema: ValueProvider = + { + provide: ZOD_POST_RELATIONSHIP_SCHEMA, + useValue: zodPostRelationship, + }; + +export const ZodInputPatchRelationshipSchema: ValueProvider = + { + provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, + useValue: zodPatchRelationship, + }; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts new file mode 100644 index 00000000..72ff9d08 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts @@ -0,0 +1,165 @@ +import { + METHOD_METADATA, + PATH_METADATA, + ROUTE_ARGS_METADATA, +} from '@nestjs/common/constants'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; + +import { bindController } from './bind-controller'; +import { Users } from '../../../mock-utils'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; +import { + ParseIntPipe, + Query, + Body, + Param, + PipeTransform, + ArgumentMetadata, +} from '@nestjs/common'; +import { OrmService } from '../types'; +import { PatchData } from '../zod'; +import { JsonApi } from '../decorators'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { excludeMethod } from '../config/bindings'; + +import { Bindings } from '../config/bindings'; + +const mapParams = new Map(); +mapParams.set(Query, RouteParamtypes.QUERY); +mapParams.set(Body, RouteParamtypes.BODY); +mapParams.set(Param, RouteParamtypes.PARAM); + +describe('bindController', () => { + it('Should be all methode', () => { + class Controller {} + const config = { + requiredSelectField: false, + pipeForId: ParseIntPipe, + debug: false, + useSoftDelete: false, + } as any; + bindController(Controller, Users, config); + + expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ + 'constructor', + 'getAll', + 'getOne', + 'deleteOne', + 'postOne', + 'patchOne', + 'getRelationship', + 'postRelationship', + 'deleteRelationship', + 'patchRelationship', + ]); + + for (const [key, value] of ObjectTyped.entries(Bindings)) { + const descriptor = Reflect.getOwnPropertyDescriptor( + Controller.prototype, + key + ); + if (!descriptor) { + throw new Error('descriptor is empty:' + key); + } + + expect(Reflect.getMetadata(PATH_METADATA, descriptor.value)).toBe( + value.path + ); + expect(Reflect.getMetadata(METHOD_METADATA, descriptor.value)).toBe( + value.method + ); + const paramsMetadata = Reflect.getMetadata( + ROUTE_ARGS_METADATA, + Controller.prototype.constructor, + key + ); + for (const params in value.parameters) { + const tmp = value.parameters[params]; + if (!tmp.decorator) { + expect(paramsMetadata).toEqual(tmp.decorator); + continue; + } + const paramsMetadataItem = + paramsMetadata[`${mapParams.get(tmp.decorator)}:${params}`]; + expect(paramsMetadataItem).not.toEqual(undefined); + expect(paramsMetadataItem.index).toBe(parseInt(params)); + tmp.mixins.forEach((i, k) => { + expect(i(Users, config).name).toEqual( + paramsMetadataItem.pipes[k].name + ); + }); + } + } + }); + + it('Should be without methode: postOne, getRelationship', () => { + @JsonApi(Users, { + allowMethod: excludeMethod(['postOne', 'getRelationship']), + }) + class Controller {} + const config = { + requiredSelectField: false, + pipeForId: ParseIntPipe, + debug: false, + useSoftDelete: false, + } as any; + bindController(Controller, Users, config); + expect(Object.getOwnPropertyNames(Controller.prototype)).toEqual([ + 'constructor', + 'getAll', + 'getOne', + 'deleteOne', + 'patchOne', + 'postRelationship', + 'deleteRelationship', + 'patchRelationship', + ]); + }); + + it('Should be use custom pipe', () => { + class SomePipes implements PipeTransform { + transform(value: any, metadata: ArgumentMetadata): any { + return undefined; + } + } + class Controller extends JsonBaseController { + override patchOne( + @Param('id', SomePipes) id: string | number, + @Body(SomePipes) inputData: PatchData + ): ReturnType['patchOne']> { + return super.patchOne(id, inputData); + } + } + const config = { + requiredSelectField: false, + pipeForId: SomePipes, + debug: false, + useSoftDelete: false, + } as any; + bindController(Controller, Users, config); + + const paramsMetadata = Reflect.getMetadata( + ROUTE_ARGS_METADATA, + Controller.prototype.constructor, + 'patchOne' + ); + expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes[0]).toEqual( + SomePipes + ); + + expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes.length).toBe( + Bindings.patchOne.parameters[0].mixins.length + 1 + ); + expect(paramsMetadata[`${mapParams.get(Param)}:0`].pipes.at(-1)).toEqual( + SomePipes + ); + + expect(paramsMetadata[`${mapParams.get(Body)}:1`].pipes.length).toBe( + Bindings.patchOne.parameters[1].mixins.length + 1 + ); + expect(paramsMetadata[`${mapParams.get(Body)}:1`].pipes.at(-1)).toEqual( + SomePipes + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts new file mode 100644 index 00000000..de5c6d66 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.ts @@ -0,0 +1,124 @@ +import { + Body, + Delete, + Get, + HttpCode, + Param, + Patch, + Post, + Query, + RequestMethod, +} from '@nestjs/common'; +import { ROUTE_ARGS_METADATA } from '@nestjs/common/constants'; +import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; + +import { Bindings } from '../config/bindings'; +import { DecoratorOptions, MixinOptions, MethodName } from '../types'; +import { NestController, ExtractNestType } from '../../../types'; +import { JSON_API_DECORATOR_OPTIONS } from '../../../constants'; + +export function bindController( + controller: ExtractNestType, + entity: MixinOptions['entity'], + config: MixinOptions['config'] +): void { + for (const methodName in Bindings) { + const { name, path, parameters, method, implementation } = + Bindings[methodName as MethodName]; + + const decoratorOptions: DecoratorOptions = Reflect.getMetadata( + JSON_API_DECORATOR_OPTIONS, + controller + ); + if (decoratorOptions) { + const { allowMethod = Object.keys(Bindings) } = decoratorOptions; + if (!allowMethod.includes(name)) continue; + } + + if (!Object.prototype.hasOwnProperty.call(controller.prototype, name)) { + // need uniq descriptor for correct work swagger + Reflect.defineProperty(controller.prototype, name, { + value: function ( + ...arg: Parameters + ): ReturnType { + return this.constructor.__proto__.prototype[name].call(this, ...arg); + }, + writable: true, + enumerable: false, + configurable: true, + }); + } + + const descriptor = Reflect.getOwnPropertyDescriptor( + controller.prototype, + name + ); + + if (!descriptor) { + throw new Error( + `Descriptor for "${controller.name}[${name}]" is undefined` + ); + } + + switch (method) { + case RequestMethod.GET: { + Get(path)(controller.prototype, name, descriptor); + break; + } + case RequestMethod.DELETE: { + HttpCode(204)(controller.prototype, name, descriptor); + Delete(path)(controller.prototype, name, descriptor); + break; + } + case RequestMethod.POST: { + Post(path)(controller.prototype, name, descriptor); + break; + } + case RequestMethod.PATCH: { + Patch(path)(controller.prototype, name, descriptor); + break; + } + default: { + throw new Error(`Method '${method}' unsupported`); + } + } + const paramsMetadata = Reflect.getMetadata( + ROUTE_ARGS_METADATA, + controller.prototype.constructor, + name + ); + + for (const key in parameters) { + const parameter = parameters[key]; + const { property, decorator, mixins } = parameter; + const resultMixin = mixins.map((mixin) => mixin(entity, config)); + + if (paramsMetadata) { + let typeDecorator: RouteParamtypes; + switch (decorator) { + case Query: + typeDecorator = RouteParamtypes.QUERY; + break; + case Param: + typeDecorator = RouteParamtypes.PARAM; + break; + case Body: + typeDecorator = RouteParamtypes.BODY; + } + + const tmp = Object.entries(paramsMetadata) + .filter(([k, v]) => k.split(':').at(0) === typeDecorator.toString()) + .reduce( + (acum, [k, v]) => (acum.push(...(v as any).pipes), acum), + [] as any + ); + resultMixin.push(...tmp); + } + decorator(property, ...resultMixin)( + controller.prototype, + name, + parseInt(key, 10) + ); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts new file mode 100644 index 00000000..8ca13ac8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts @@ -0,0 +1,95 @@ +import { + CONTROLLER_WATERMARK, + INTERCEPTORS_METADATA, + PATH_METADATA, + PROPERTY_DEPS_METADATA, +} from '@nestjs/common/constants'; +import { createController } from './create-controller'; +import { Users } from '../../../mock-utils'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { + JSON_API_CONTROLLER_POSTFIX, + ORM_SERVICE, + ORM_SERVICE_PROPS, +} from '../../../constants'; +import { InjectService, JsonApi } from '../decorators'; +import { ErrorInterceptors, LogTimeInterceptors } from '../interceptors'; + +describe('createController', () => { + it('Should be error', () => { + class TestController {} + expect.assertions(2); + try { + createController(Users, TestController); + } catch (e) { + expect(e).toBeInstanceOf(Error); + expect((e as Error).message).toBe( + 'Controller "TestController" should be inherited of "JsonBaseController"' + ); + } + }); + + it('Should be correct name controller', () => { + class TestController extends JsonBaseController {} + const result = createController(Users); + const result1 = createController(Users, TestController); + expect(result.name).toBe('Users' + JSON_API_CONTROLLER_POSTFIX); + expect(result1.name).toBe('TestController'); + }); + + it('Should be correct path for controller', () => { + const overrideRoute = 'override-route'; + class TestController extends JsonBaseController {} + + @JsonApi(Users, { + overrideRoute, + }) + class TestController2 extends JsonBaseController {} + const result = createController(Users); + const result2 = createController(Users, TestController); + const result3 = createController(Users, TestController2); + + expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result)).toBe(true); + expect(Reflect.getMetadata(PATH_METADATA, result)).toBe('users'); + + expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result2)).toBe(true); + expect(Reflect.getMetadata(PATH_METADATA, result2)).toBe('users'); + + expect(Reflect.getMetadata(CONTROLLER_WATERMARK, result3)).toBe(true); + expect(Reflect.getMetadata(PATH_METADATA, result3)).toBe(overrideRoute); + }); + + it('Check inject typeorm, service', () => { + class TestController extends JsonBaseController { + @InjectService() private tmp: any; + } + + const result = createController(Users); + const result1 = createController(Users, TestController); + + const check = Reflect.getMetadata( + PROPERTY_DEPS_METADATA, + result.prototype.constructor + ); + const check1 = Reflect.getMetadata( + PROPERTY_DEPS_METADATA, + result1.prototype.constructor + ); + + const intecept = Reflect.getMetadata( + INTERCEPTORS_METADATA, + result1.prototype.constructor + ); + expect(intecept).not.toBe(undefined); + expect(intecept[0]).toEqual(LogTimeInterceptors); + expect(intecept[1]).toEqual(ErrorInterceptors); + expect(check[0].key).toBe(ORM_SERVICE_PROPS); + expect(check[0].type).toEqual(ORM_SERVICE); + + expect(check1[0].key).toBe('tmp'); + expect(check1[0].type).toEqual(ORM_SERVICE); + + expect(check1[1].key).toBe(ORM_SERVICE_PROPS); + expect(check1[1].type).toEqual(ORM_SERVICE); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts new file mode 100644 index 00000000..bab373a3 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts @@ -0,0 +1,53 @@ +import { Controller, Inject, Type, UseInterceptors } from '@nestjs/common'; + +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; + +import { getProviderName, nameIt } from './utils'; +import { + JSON_API_CONTROLLER_POSTFIX, + JSON_API_DECORATOR_OPTIONS, + ORM_SERVICE, + ORM_SERVICE_PROPS, +} from '../../../constants'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { ErrorInterceptors, LogTimeInterceptors } from '../interceptors'; + +import { DecoratorOptions, MixinOptions } from '../types'; + +export function createController( + entity: MixinOptions['entity'], + controller?: MixinOptions['controller'] +): Type { + const controllerClass = + controller || + nameIt( + getProviderName(entity, JSON_API_CONTROLLER_POSTFIX), + JsonBaseController + ); + + const entityName = entity.name; + + if ( + !Object.prototype.isPrototypeOf.call(JsonBaseController, controllerClass) + ) { + throw new Error( + `Controller "${controller?.name}" should be inherited of "JsonBaseController"` + ); + } + + const decoratorOptions: DecoratorOptions = Reflect.getMetadata( + JSON_API_DECORATOR_OPTIONS, + controllerClass + ); + + const controllerPath = + decoratorOptions && decoratorOptions['overrideRoute'] + ? decoratorOptions['overrideRoute'].toString() + : `${camelToKebab(entityName)}`; + Controller(controllerPath)(controllerClass); + + Inject(ORM_SERVICE)(controllerClass.prototype, ORM_SERVICE_PROPS); + UseInterceptors(LogTimeInterceptors)(controllerClass); + UseInterceptors(ErrorInterceptors)(controllerClass); + return controllerClass; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts new file mode 100644 index 00000000..7f0e8bc2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/index.ts @@ -0,0 +1,3 @@ +export * from './utils'; +export * from './create-controller'; +export * from './bind-controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts new file mode 100644 index 00000000..fd1d708e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.spec.ts @@ -0,0 +1,17 @@ +import { getEntityName, nameIt } from './'; + +describe('Test utils', () => { + it('getEntityName', () => { + expect(getEntityName('Entity')).toBe('Entity'); + expect(getEntityName(class EntityClass {})).toBe('EntityClass'); + class EntityClassInst {} + const tmp = new EntityClassInst(); + expect(getEntityName(tmp as any)).toBe('EntityClassInst'); + }); + + it('nameIt', () => { + const newNameClass = 'newNameClass'; + const newClass = nameIt(newNameClass, class {}); + expect(getEntityName(newClass)).toBe(newNameClass); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts new file mode 100644 index 00000000..8aa2b798 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts @@ -0,0 +1,41 @@ +import { EntityTarget, ObjectLiteral } from '../../../types'; + +import { upperFirstLetter } from '@klerick/json-api-nestjs-shared'; + +export const nameIt = ( + name: string, + cls: new (...rest: unknown[]) => Record +) => + ({ + [name]: class extends cls { + constructor(...arg: unknown[]) { + super(...arg); + } + }, + }[name]); + +export const getEntityName = ( + entity: EntityTarget +): string => { + if (typeof entity === 'string') { + return entity; + } + + if ('name' in entity) { + return entity['name']; + } + + if ('constructor' in entity && 'name' in entity.constructor) { + return entity['constructor']['name']; + } + + return `${entity}`; +}; + +export function getProviderName( + entity: EntityTarget, + name: string +) { + const entityName = getEntityName(entity); + return `${upperFirstLetter(entityName)}${name}`; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts new file mode 100644 index 00000000..4cba8fcf --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts @@ -0,0 +1,120 @@ +import { + InternalServerErrorException, + CallHandler, + ExecutionContext, + Inject, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { catchError, Observable, throwError } from 'rxjs'; +import { ObjectLiteral } from '../../../types'; +// import { QueryFailedError, Repository } from 'typeorm'; +// import { DriverUtils } from 'typeorm/driver/DriverUtils'; +// +// import { +// CONTROL_OPTIONS_TOKEN, +// CURRENT_ENTITY_REPOSITORY, +// } from '../../constants'; +// import { ConfigParam, Entity, ValidateQueryError } from '../../types'; +// import { +// MysqlError, +// MysqlErrorCode, +// PostgresError, +// PostgresErrorCode, +// } from '../../helper'; +// import { HttpException } from '@nestjs/common'; + +@Injectable() +export class ErrorInterceptors + implements NestInterceptor +{ + // @Inject(CURRENT_ENTITY_REPOSITORY) private repository!: Repository; + // @Inject(CONTROL_OPTIONS_TOKEN) private config!: ConfigParam; + + intercept( + context: ExecutionContext, + next: CallHandler + ): Observable | Promise> { + return next.handle(); + // return next.handle().pipe( + // catchError((error) => { + // if (error instanceof QueryFailedError) { + // return throwError(() => this.prepareDataBaseError(error)); + // } + // + // if (error instanceof HttpException) { + // return throwError(() => error); + // } + // + // const errorObject: ValidateQueryError = { + // code: 'internal_error', + // message: this.config.debug ? error.message : 'Internal Server Error', + // path: [], + // }; + // const descriptionOrOptions = this.config.debug ? error : undefined; + // return throwError( + // () => + // new InternalServerErrorException( + // [errorObject], + // descriptionOrOptions + // ) + // ); + // }) + // ); + } + + // private prepareDataBaseError(error: QueryFailedError): HttpException { + // const errorObject: ValidateQueryError = { + // code: 'internal_error', + // message: this.config.debug ? error.message : 'Internal Server Error', + // path: [], + // }; + // + // if (DriverUtils.isMySQLFamily(this.repository.manager.connection.driver)) { + // const { errorCode, errorMsg } = this.prepareMysqlError(error.driverError); + // if (MysqlError[errorCode]) { + // return MysqlError[errorCode](this.repository.metadata, errorMsg); + // } + // } + // + // if ( + // DriverUtils.isPostgresFamily(this.repository.manager.connection.driver) + // ) { + // const { errorCode, errorMsg, detail } = this.preparePostgresError( + // error.driverError + // ); + // + // if (PostgresError[errorCode]) { + // return PostgresError[errorCode]( + // this.repository.metadata, + // errorMsg, + // detail + // ); + // } + // } + // + // return new InternalServerErrorException([errorObject]); + // } + // + // private prepareMysqlError(error: any): { + // errorCode: MysqlErrorCode; + // errorMsg: string; + // } { + // return { + // errorCode: error.errno, + // errorMsg: error.message, + // }; + // } + // + // private preparePostgresError(error: any): { + // errorCode: PostgresErrorCode; + // errorMsg: string; + // detail: string; + // } { + // return { + // errorCode: error.code, + // errorMsg: error.message, + // detail: error.detail, + // }; + // } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts new file mode 100644 index 00000000..b6030081 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/index.ts @@ -0,0 +1,2 @@ +export * from './error.interceptors'; +export * from './log-time.interceptors'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts new file mode 100644 index 00000000..2b6b2797 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/log-time.interceptors.ts @@ -0,0 +1,30 @@ +import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common'; +import { map, Observable } from 'rxjs'; +import { ObjectLiteral } from '../../../types'; + +// import { Entity, ResourceObject } from '../../types'; + +export class LogTimeInterceptors + implements NestInterceptor +{ + intercept( + context: ExecutionContext, + // next: CallHandler> + next: CallHandler + ): Observable | Promise> { + const now = Date.now(); + return next.handle(); + // .pipe( + // map((r) => { + // const response = context.switchToHttp().getResponse(); + // const time = Date.now() - now; + // response.setHeader('x-response-time', time); + // if (r && r.meta) { + // r.meta['time'] = time; + // } + // + // return r; + // }) + // ); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts new file mode 100644 index 00000000..7d4ea222 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts @@ -0,0 +1,84 @@ +import { DynamicModule } from '@nestjs/common'; + +import { MixinOptions, DecoratorOptions } from './types'; +import { createController } from './helper'; +import { + JSON_API_DECORATOR_OPTIONS, + CONTROL_OPTIONS_TOKEN, + JSON_API_MODULE_POSTFIX, + CURRENT_ENTITY, + FIND_ONE_ROW_ENTITY, + CHECK_RELATION_NAME, +} from '../../constants'; +import { ConfigParam, RequiredFromPartial } from '../../types'; +import { MicroOrmParam } from '../micro-orm'; +import { TypeOrmParam } from '../type-orm'; +import { bindController, getProviderName, nameIt } from './helper'; +import { + ZodInputQuerySchema, + ZodPostSchema, + ZodQuerySchema, + ZodPatchSchema, + ZodInputPatchRelationshipSchema, + ZodInputPostRelationshipSchema, +} from './factory'; + +export class MixinModule { + static forRoot(options: MixinOptions): DynamicModule { + const { entity, controller, imports, ormModule } = options; + const controllerClass = createController(entity, controller); + + const decoratorOptions: DecoratorOptions = + Reflect.getMetadata(JSON_API_DECORATOR_OPTIONS, controllerClass) || {}; + + const moduleConfig: RequiredFromPartial< + ConfigParam & (MicroOrmParam | TypeOrmParam) + > = { + ...options.config, + ...decoratorOptions, + }; + + bindController(controllerClass, entity, moduleConfig); + const optionProvider = { + provide: CONTROL_OPTIONS_TOKEN, + useValue: moduleConfig, + }; + + const currentEntityProvider = { + provide: CURRENT_ENTITY, + useValue: entity, + }; + + const findOneRowEntityProvider = { + provide: FIND_ONE_ROW_ENTITY, + useValue: undefined, + }; + + const checkRelationNameProvider = { + provide: CHECK_RELATION_NAME, + useValue: undefined, + }; + + return { + module: nameIt( + getProviderName(entity, JSON_API_MODULE_POSTFIX), + MixinModule + ), + controllers: [controllerClass], + providers: [ + optionProvider, + currentEntityProvider, + findOneRowEntityProvider, + checkRelationNameProvider, + ...ormModule.getUtilProviders(entity), + ZodInputQuerySchema(), + ZodQuerySchema(), + ZodPatchSchema(), + ZodPostSchema(), + ZodInputPatchRelationshipSchema, + ZodInputPostRelationshipSchema, + ], + imports: imports, + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts new file mode 100644 index 00000000..a8e52f7d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.spec.ts @@ -0,0 +1,55 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundException } from '@nestjs/common'; +import { CheckItemEntityPipe } from './check-item-entity.pipe'; +import { CURRENT_ENTITY, FIND_ONE_ROW_ENTITY } from '../../../../constants'; +import { EntityTarget } from 'typeorm/common/EntityTarget'; + +describe('CheckItemEntityPipe', () => { + let pipe: CheckItemEntityPipe; + let mockFindOneRowEntity: jest.Mock; + let mockEntityTarget: EntityTarget; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CheckItemEntityPipe, + { provide: CURRENT_ENTITY, useValue: {} }, + { provide: FIND_ONE_ROW_ENTITY, useValue: jest.fn() }, + ], + }).compile(); + + pipe = module.get>(CheckItemEntityPipe); + mockEntityTarget = module.get>(CURRENT_ENTITY); + mockFindOneRowEntity = module.get(FIND_ONE_ROW_ENTITY); + }); + + it('should call findOneRowEntity and return the entity', async () => { + const mockEntity = { id: 1, name: 'Test Entity' }; + const mockValue = 1; + + mockFindOneRowEntity.mockResolvedValue(mockEntity); + const result = await pipe.transform(mockValue); + + expect(mockFindOneRowEntity).toHaveBeenCalledTimes(1); + expect(mockFindOneRowEntity).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + + expect(result).toBe(mockValue); + }); + + it('should throw a NotFoundException if no entity is found', async () => { + const mockValue = 1; + + mockFindOneRowEntity.mockResolvedValue(null); + + await expect(pipe.transform(mockValue)).rejects.toThrow(NotFoundException); + + expect(mockFindOneRowEntity).toHaveBeenCalledTimes(1); + expect(mockFindOneRowEntity).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts new file mode 100644 index 00000000..8244d3aa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/check-item-entity.pipe.ts @@ -0,0 +1,34 @@ +import { Inject, NotFoundException, PipeTransform } from '@nestjs/common'; +import { ValidateQueryError } from '../../../../types'; +import { CURRENT_ENTITY, FIND_ONE_ROW_ENTITY } from '../../../../constants'; +import { EntityTarget, ObjectLiteral } from '../../../../types'; +import { FindOneRowEntity } from '../../types'; +import { getEntityName } from '../../helper'; + +export class CheckItemEntityPipe< + E extends ObjectLiteral, + I extends string | number +> implements PipeTransform> +{ + @Inject(CURRENT_ENTITY) private currentEntity!: EntityTarget; + @Inject(FIND_ONE_ROW_ENTITY) private findOneRowEntity!: + | FindOneRowEntity + | undefined; + async transform(value: I): Promise { + if (!this.findOneRowEntity || typeof this.findOneRowEntity !== 'function') + return value; + + const result = await this.findOneRowEntity(this.currentEntity, value); + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${getEntityName( + this.currentEntity + )}' with id '${value}' does not exist`, + path: [], + }; + throw new NotFoundException([error]); + } + return value; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts new file mode 100644 index 00000000..256cdb57 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/check-item-entity/index.ts @@ -0,0 +1 @@ +export * from './check-item-entity.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts new file mode 100644 index 00000000..6970b810 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.spec.ts @@ -0,0 +1,46 @@ +import { INJECTABLE_WATERMARK } from '@nestjs/common/constants'; +import { PipeTransform } from '@nestjs/common/interfaces'; + +import { MixinOptions } from '../types'; +import { factoryMixin } from './index'; + +describe('factoryMixin', () => { + class TestEntity {} + + class TestPipe implements PipeTransform { + name = 'TestPipe'; + + transform(value: any) { + return value; + } + } + + function isInjectable(cls: any): boolean { + return Reflect.getMetadata(INJECTABLE_WATERMARK, cls); + } + + it('should return a pipe class with a new name', () => { + const pipeClass = factoryMixin( + TestEntity as MixinOptions['entity'], + TestPipe + ); + expect(pipeClass.name).toBe('TestEntityTestPipe'); + }); + + it('should return a pipe class that is Injectable', () => { + const pipeClass = factoryMixin( + TestEntity as MixinOptions['entity'], + TestPipe + ); + expect(isInjectable(pipeClass)).toBe(true); + }); + + it('should preserve the behavior of the original pipe', () => { + const pipeClass = factoryMixin( + TestEntity as MixinOptions['entity'], + TestPipe + ); + const instance = new pipeClass(); + expect(instance.transform('testValue', {} as any)).toBe('testValue'); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts new file mode 100644 index 00000000..ee2bf865 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts @@ -0,0 +1,89 @@ +import { Injectable, ParseIntPipe } from '@nestjs/common'; +import { upperFirstLetter } from '@klerick/json-api-nestjs-shared'; + +import { PipeMixin } from '../../../types'; +import { MixinOptions } from '../types'; +import { nameIt } from '../helper'; + +import { QueryInputPipe } from './query-input'; +import { QueryPipe } from './query'; +import { QueryFiledInIncludePipe } from './query-filed-on-include'; +import { QueryCheckSelectField } from './query-check-select-field'; +import { CheckItemEntityPipe } from './check-item-entity'; +import { PostInputPipe } from './post-input'; +import { PatchInputPipe } from './patch-input'; +import { ParseRelationshipNamePipe } from './parse-relationship-name'; +import { PostRelationshipPipe } from './post-relationship'; +import { PatchRelationshipPipe } from './patch-relationship'; + +export function factoryMixin(entity: MixinOptions['entity'], pipe: PipeMixin) { + const entityName = entity.name; + + const pipeClass = nameIt( + `${upperFirstLetter(entityName)}${pipe.name}`, + pipe + ) as PipeMixin; + + Injectable()(pipeClass); + + return pipeClass; +} + +export function queryInputMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, QueryInputPipe); +} + +export function queryMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, QueryPipe); +} + +export function queryFiledInIncludeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, QueryFiledInIncludePipe); +} + +export function queryCheckSelectFieldMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, QueryCheckSelectField); +} + +export function idPipeMixin( + entity: MixinOptions['entity'], + config?: MixinOptions['config'] +): PipeMixin { + return config && config.pipeForId ? config.pipeForId : (ParseIntPipe as any); +} + +export function checkItemEntityPipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, CheckItemEntityPipe); +} + +export function postInputPipeMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, PostInputPipe); +} + +export function patchInputPipeMixin(entity: MixinOptions['entity']): PipeMixin { + return factoryMixin(entity, PatchInputPipe); +} + +export function postRelationshipPipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, PostRelationshipPipe); +} + +export function patchRelationshipPipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, PatchRelationshipPipe); +} + +export function parseRelationshipNamePipeMixin( + entity: MixinOptions['entity'] +): PipeMixin { + return factoryMixin(entity, ParseRelationshipNamePipe); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts new file mode 100644 index 00000000..62c4518d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/index.ts @@ -0,0 +1 @@ +export * from './parse-relationship-name.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts new file mode 100644 index 00000000..2709180b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.spec.ts @@ -0,0 +1,58 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UnprocessableEntityException } from '@nestjs/common'; +import { ParseRelationshipNamePipe } from './parse-relationship-name.pipe'; +import { CURRENT_ENTITY, CHECK_RELATION_NAME } from '../../../../constants'; +import { EntityTarget } from 'typeorm/common/EntityTarget'; + +describe('CheckItemEntityPipe', () => { + let pipe: ParseRelationshipNamePipe; + let checkRelationNameMock: jest.Mock; + let mockEntityTarget: EntityTarget; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ParseRelationshipNamePipe, + { provide: CURRENT_ENTITY, useValue: {} }, + { provide: CHECK_RELATION_NAME, useValue: jest.fn() }, + ], + }).compile(); + + pipe = module.get>( + ParseRelationshipNamePipe + ); + mockEntityTarget = module.get>(CURRENT_ENTITY); + checkRelationNameMock = module.get(CHECK_RELATION_NAME); + }); + + it('should call findOneRowEntity and return the entity', async () => { + const mockValue = 'name'; + + checkRelationNameMock.mockReturnValueOnce(true); + const result = await pipe.transform(mockValue); + + expect(checkRelationNameMock).toHaveBeenCalledTimes(1); + expect(checkRelationNameMock).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + + expect(result).toBe(mockValue); + }); + + it('should throw a UnprocessableEntityException if no entity is found', async () => { + const mockValue = 'name'; + + checkRelationNameMock.mockReturnValueOnce(false); + + expect(() => pipe.transform(mockValue)).toThrow( + UnprocessableEntityException + ); + + expect(checkRelationNameMock).toHaveBeenCalledTimes(1); + expect(checkRelationNameMock).toHaveBeenCalledWith( + mockEntityTarget, + mockValue + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts new file mode 100644 index 00000000..2224ec0b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts @@ -0,0 +1,38 @@ +import { + PipeTransform, + UnprocessableEntityException, + Inject, +} from '@nestjs/common'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { ValidateQueryError } from '../../../../types'; +import { CHECK_RELATION_NAME, CURRENT_ENTITY } from '../../../../constants'; +import { EntityTarget, ObjectLiteral } from '../../../../types'; +import { CheckRelationNme } from '../../types'; +import { getEntityName } from '../../helper'; + +export class ParseRelationshipNamePipe< + E extends ObjectLiteral, + I extends EntityRelation +> implements PipeTransform +{ + @Inject(CURRENT_ENTITY) private currentEntity!: EntityTarget; + @Inject(CHECK_RELATION_NAME) private checkRelationName!: CheckRelationNme; + + transform(value: string): I { + if (!this.checkRelationName || typeof this.checkRelationName !== 'function') + return value as I; + + const result = this.checkRelationName(this.currentEntity, value); + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Relation '${value}' does not exist in resource '${getEntityName( + this.currentEntity + )}'`, + path: [], + }; + throw new UnprocessableEntityException([error]); + } + return value as I; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts new file mode 100644 index 00000000..6d849c7a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/index.ts @@ -0,0 +1 @@ +export * from './patch-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts new file mode 100644 index 00000000..7e54641f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.spec.ts @@ -0,0 +1,69 @@ +import { ZodError } from 'zod'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; + +import { PatchInputPipe } from './patch-input.pipe'; +import { PostData, ZodPost } from '../../zod'; +import { JSONValue } from '../../types'; +import { ZOD_PATCH_SCHEMA } from '../../../../constants'; + +type MockEntity = { id: number; name: string }; + +describe('PostInputPipe', () => { + let pipe: PatchInputPipe; + let mockSchema: ZodPost; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PatchInputPipe, + { + provide: ZOD_PATCH_SCHEMA, + useValue: { + parse: jest.fn(), + }, + }, + ], + }).compile(); + + pipe = module.get>(PatchInputPipe); + mockSchema = module.get>(ZOD_PATCH_SCHEMA); + }); + + it('should transform JSONValue to PostData on success', () => { + const input: JSONValue = { key: 'value' } as any; + const expectedData: PostData = { id: 1, key: 'value' } as any; + + jest + .spyOn(mockSchema, 'parse') + .mockReturnValue({ data: expectedData } as any); + + expect(pipe.transform(input)).toEqual(expectedData); + expect(mockSchema.parse).toHaveBeenCalledWith(input, { + errorMap: expect.any(Function), + }); + }); + + it('should throw BadRequestException if ZodError occurs', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new ZodError([]); + }); + + expect(() => pipe.transform(input)).toThrow(BadRequestException); + }); + + it('should throw InternalServerErrorException for non-Zod errors', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new Error('Unexpected Error'); + }); + + expect(() => pipe.transform(input)).toThrow(InternalServerErrorException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts new file mode 100644 index 00000000..3fd108af --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-input/patch-input.pipe.ts @@ -0,0 +1,33 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; + +import { JSONValue } from '../../types'; +import { PatchData, ZodPatch } from '../../zod'; +import { ZOD_PATCH_SCHEMA } from '../../../../constants'; +import { ObjectLiteral } from '../../../../types'; + +export class PatchInputPipe + implements PipeTransform> +{ + @Inject(ZOD_PATCH_SCHEMA) + private zodInputPatchSchema!: ZodPatch; + transform(value: JSONValue): PatchData { + try { + return this.zodInputPatchSchema.parse(value, { + errorMap: errorMap, + })['data'] as PatchData; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts new file mode 100644 index 00000000..99ef16e8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/index.ts @@ -0,0 +1 @@ +export * from './patch-relationship.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts new file mode 100644 index 00000000..8c591419 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.spec.ts @@ -0,0 +1,84 @@ +import { IMemoryDb } from 'pg-mem'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + InternalServerErrorException, + BadRequestException, +} from '@nestjs/common'; + +import { ZOD_PATCH_RELATIONSHIP_SCHEMA } from '../../../../constants'; + +import { PatchRelationshipPipe } from './patch-relationship.pipe'; +import { ZodPatchRelationship } from '../../zod'; +import { ZodError } from 'zod'; + +describe('PatchInputPipe', () => { + let patchRelationshipPipe: PatchRelationshipPipe; + let zodInputPatchRelationshipSchema: ZodPatchRelationship; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ZOD_PATCH_RELATIONSHIP_SCHEMA, + useValue: { + parse() {}, + }, + }, + PatchRelationshipPipe, + ], + }).compile(); + + patchRelationshipPipe = module.get( + PatchRelationshipPipe + ); + zodInputPatchRelationshipSchema = module.get( + ZOD_PATCH_RELATIONSHIP_SCHEMA + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('It should be ok', () => { + const data = { + some: 'data', + }; + const check = { + data, + }; + jest + .spyOn(zodInputPatchRelationshipSchema, 'parse') + .mockImplementationOnce(() => check as any); + expect(patchRelationshipPipe.transform(check)).toEqual(data); + }); + + it('Should be not ok', () => { + jest + .spyOn(zodInputPatchRelationshipSchema, 'parse') + .mockImplementationOnce(() => { + throw new ZodError([]); + }); + expect.assertions(1); + try { + patchRelationshipPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + it('Should be 500', () => { + jest + .spyOn(zodInputPatchRelationshipSchema, 'parse') + .mockImplementationOnce(() => { + throw new Error('Error mock'); + }); + expect.assertions(1); + + try { + patchRelationshipPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(InternalServerErrorException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts new file mode 100644 index 00000000..bf23ae36 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/patch-relationship/patch-relationship.pipe.ts @@ -0,0 +1,32 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; + +import { JSONValue } from '../../types'; +import { PatchRelationshipData, ZodPatchRelationship } from '../../zod'; +import { ZOD_PATCH_RELATIONSHIP_SCHEMA } from '../../../../constants'; + +export class PatchRelationshipPipe + implements PipeTransform +{ + @Inject(ZOD_PATCH_RELATIONSHIP_SCHEMA) + private zodInputPatchRelationshipSchema!: ZodPatchRelationship; + transform(value: JSONValue): PatchRelationshipData { + try { + return this.zodInputPatchRelationshipSchema.parse(value, { + errorMap: errorMap, + })['data']; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts new file mode 100644 index 00000000..58efc255 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/index.ts @@ -0,0 +1 @@ +export * from './post-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts new file mode 100644 index 00000000..01481275 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.spec.ts @@ -0,0 +1,69 @@ +import { ZodError } from 'zod'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; + +import { PostInputPipe } from './post-input.pipe'; +import { PostData, ZodPost } from '../../zod'; +import { JSONValue } from '../../types'; +import { ZOD_POST_SCHEMA } from '../../../../constants'; + +type MockEntity = { id: number; name: string }; + +describe('PostInputPipe', () => { + let pipe: PostInputPipe; + let mockSchema: ZodPost; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PostInputPipe, + { + provide: ZOD_POST_SCHEMA, + useValue: { + parse: jest.fn(), + }, + }, + ], + }).compile(); + + pipe = module.get>(PostInputPipe); + mockSchema = module.get>(ZOD_POST_SCHEMA); + }); + + it('should transform JSONValue to PostData on success', () => { + const input: JSONValue = { key: 'value' } as any; + const expectedData: PostData = { id: 1, key: 'value' } as any; + + jest + .spyOn(mockSchema, 'parse') + .mockReturnValue({ data: expectedData } as any); + + expect(pipe.transform(input)).toEqual(expectedData); + expect(mockSchema.parse).toHaveBeenCalledWith(input, { + errorMap: expect.any(Function), + }); + }); + + it('should throw BadRequestException if ZodError occurs', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new ZodError([]); + }); + + expect(() => pipe.transform(input)).toThrow(BadRequestException); + }); + + it('should throw InternalServerErrorException for non-Zod errors', () => { + const input: JSONValue = { key: 'value' }; + + jest.spyOn(mockSchema, 'parse').mockImplementation(() => { + throw new Error('Unexpected Error'); + }); + + expect(() => pipe.transform(input)).toThrow(InternalServerErrorException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts new file mode 100644 index 00000000..5059b8ce --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-input/post-input.pipe.ts @@ -0,0 +1,32 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; + +import { PostData, ZodPost } from '../../zod'; +import { ZOD_POST_SCHEMA } from '../../../../constants'; +import { ObjectLiteral } from '../../../../types'; +import { JSONValue } from '../../types'; + +export class PostInputPipe + implements PipeTransform> +{ + @Inject(ZOD_POST_SCHEMA) private zodInputPostSchema!: ZodPost; + transform(value: JSONValue): PostData { + try { + return this.zodInputPostSchema.parse(value, { + errorMap: errorMap, + })['data'] as PostData; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts new file mode 100644 index 00000000..70457a78 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/index.ts @@ -0,0 +1 @@ +export * from './post-relationship.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts new file mode 100644 index 00000000..8833734d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.spec.ts @@ -0,0 +1,83 @@ +import { IMemoryDb } from 'pg-mem'; +import { Test, TestingModule } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { + InternalServerErrorException, + BadRequestException, +} from '@nestjs/common'; +import { ZOD_POST_RELATIONSHIP_SCHEMA } from '../../../../constants'; + +import { PostRelationshipPipe } from './post-relationship.pipe'; +import { ZodPostRelationship } from '../../zod'; +import { ZodError } from 'zod'; + +describe('PostInputPipe', () => { + let postRelationshipPipe: PostRelationshipPipe; + let zodInputPostRelationshipSchema: ZodPostRelationship; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ZOD_POST_RELATIONSHIP_SCHEMA, + useValue: { + parse() {}, + }, + }, + PostRelationshipPipe, + ], + }).compile(); + + postRelationshipPipe = + module.get(PostRelationshipPipe); + zodInputPostRelationshipSchema = module.get( + ZOD_POST_RELATIONSHIP_SCHEMA + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('It should be ok', () => { + const data = { + some: 'data', + }; + const check = { + data, + }; + jest + .spyOn(zodInputPostRelationshipSchema, 'parse') + .mockImplementationOnce(() => check as any); + expect(postRelationshipPipe.transform(check)).toEqual(data); + }); + + it('Should be not ok', () => { + jest + .spyOn(zodInputPostRelationshipSchema, 'parse') + .mockImplementationOnce(() => { + throw new ZodError([]); + }); + expect.assertions(1); + try { + postRelationshipPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + it('Should be 500', () => { + jest + .spyOn(zodInputPostRelationshipSchema, 'parse') + .mockImplementationOnce(() => { + throw new Error('Error mock'); + }); + expect.assertions(1); + + try { + postRelationshipPipe.transform({}); + } catch (e) { + expect(e).toBeInstanceOf(InternalServerErrorException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts new file mode 100644 index 00000000..4a9b5820 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/post-relationship/post-relationship.pipe.ts @@ -0,0 +1,32 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; + +import { JSONValue } from '../../types'; +import { PostRelationshipData, ZodPostRelationship } from '../../zod'; +import { ZOD_POST_RELATIONSHIP_SCHEMA } from '../../../../constants'; + +export class PostRelationshipPipe + implements PipeTransform +{ + @Inject(ZOD_POST_RELATIONSHIP_SCHEMA) + private zodInputPostRelationshipSchema!: ZodPostRelationship; + transform(value: JSONValue): PostRelationshipData { + try { + return this.zodInputPostRelationshipSchema.parse(value, { + errorMap: errorMap, + })['data']; + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts new file mode 100644 index 00000000..7ba4ed4e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/index.ts @@ -0,0 +1 @@ +export * from './query-check-select-field'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts new file mode 100644 index 00000000..bd87447c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts @@ -0,0 +1,73 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { BadRequestException } from '@nestjs/common'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; + +import { QueryCheckSelectField } from './query-check-select-field'; +import { Users } from '../../../../mock-utils'; +import { CONTROL_OPTIONS_TOKEN } from '../../../../constants'; +import { Query } from '../../zod'; +import { ConfigParam, ObjectLiteral } from '../../../../types'; + +function getDefaultQuery() { + const filter = { + relation: null, + target: null, + }; + const defaultQuery: Query = { + [QueryField.filter]: filter, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: 1, + number: 1, + }, + }; + + return defaultQuery; +} + +describe('QueryCheckSelectField', () => { + let queryCheckSelectField: QueryCheckSelectField; + let configParam: ConfigParam; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + QueryCheckSelectField, + ], + }).compile(); + + queryCheckSelectField = module.get>( + QueryCheckSelectField + ); + configParam = module.get(CONTROL_OPTIONS_TOKEN); + }); + + it('Is valid', () => { + const query = getDefaultQuery(); + expect(queryCheckSelectField.transform(query)).toEqual(query); + }); + + it('Is invalid', () => { + const query = getDefaultQuery(); + jest.mocked(configParam).requiredSelectField = true; + expect.assertions(1); + try { + queryCheckSelectField.transform(query); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts new file mode 100644 index 00000000..7b98707f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.ts @@ -0,0 +1,25 @@ +import { BadRequestException, Inject, PipeTransform } from '@nestjs/common'; +import { CONTROL_OPTIONS_TOKEN } from '../../../../constants'; +import { + ConfigParam, + ObjectLiteral, + ValidateQueryError, +} from '../../../../types'; +import { Query } from '../../zod'; + +export class QueryCheckSelectField + implements PipeTransform, Query> +{ + @Inject(CONTROL_OPTIONS_TOKEN) private configParam!: ConfigParam; + transform(value: Query): Query { + if (this.configParam.requiredSelectField && value.fields === null) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Fields params in query is required'`, + path: ['fields'], + }; + throw new BadRequestException([error]); + } + return value; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts new file mode 100644 index 00000000..82b1b95f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/index.ts @@ -0,0 +1 @@ +export * from './query-filed-in-include.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts new file mode 100644 index 00000000..04bdb9c4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts @@ -0,0 +1,121 @@ +import { BadRequestException } from '@nestjs/common'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { QueryFiledInIncludePipe } from './query-filed-in-include.pipe'; +import { Users } from '../../../../mock-utils'; +import { Query } from '../../zod'; + +describe('QueryFiledInIncludePipe', () => { + let queryFiledInIncludePipe: QueryFiledInIncludePipe; + + beforeAll(() => { + queryFiledInIncludePipe = new QueryFiledInIncludePipe(); + }); + + it('Should be ok', () => { + const check: Query = { + [QueryField.fields]: { + roles: ['id'], + }, + [QueryField.include]: ['roles'], + [QueryField.filter]: { + target: null, + relation: null, + }, + [QueryField.sort]: null, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + + const check2: Query = { + [QueryField.fields]: null, + [QueryField.include]: ['roles'], + [QueryField.filter]: { + target: null, + relation: { + roles: { name: { eq: 'test' } }, + }, + }, + [QueryField.sort]: null, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + + const result = queryFiledInIncludePipe.transform(check); + expect(result).toEqual(check); + const result2 = queryFiledInIncludePipe.transform(check2); + expect(result2).toEqual(check2); + }); + + it('Should be not ok', () => { + const check: Query = { + [QueryField.fields]: { + roles: ['id'], + }, + [QueryField.include]: null, + [QueryField.filter]: { + target: null, + relation: null, + }, + [QueryField.sort]: null, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + const check2: Query = { + [QueryField.fields]: { + roles: ['id'], + }, + [QueryField.include]: null, + [QueryField.filter]: { + target: null, + relation: null, + }, + [QueryField.sort]: { + addresses: { + id: 'ASC', + }, + }, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + + const check3: Query = { + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.filter]: { + target: null, + relation: { + roles: { name: { eq: 'test' } }, + }, + }, + [QueryField.sort]: null, + [QueryField.page]: { + number: 1, + size: 1, + }, + }; + expect.assertions(3); + try { + queryFiledInIncludePipe.transform(check); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + try { + queryFiledInIncludePipe.transform(check2); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + try { + queryFiledInIncludePipe.transform(check3); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts new file mode 100644 index 00000000..dc27f7f4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts @@ -0,0 +1,70 @@ +import { BadRequestException, PipeTransform } from '@nestjs/common'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { Query } from '../../zod'; + +export class QueryFiledInIncludePipe + implements PipeTransform, Query> +{ + transform(value: Query): Query { + const errors: ValidateQueryError[] = []; + + const { fields, include, sort, filter } = value; + const includeSet = new Set(); + + if (include) { + include.reduce((acum, item) => acum.add(item), includeSet); + } + + if (filter) { + const { relation } = filter; + if (relation) { + const filterRelationFields = ObjectTyped.keys(relation); + const filterFieldsErrors = filterRelationFields + .filter((i) => !includeSet.has(i.toString())) + .map((i) => ({ + code: 'invalid_intersection_types', + message: `Add '${i.toString()}' to query param 'include'`, + path: ['filter', 'relation', i.toString()], + })); + + errors.push(...filterFieldsErrors); + } + } + + if (fields) { + const { target: targetResourceFields, ...relationFields } = fields; + const selectRelationFields = ObjectTyped.keys(relationFields); + const fieldsErrors = selectRelationFields + .filter((i) => !includeSet.has(i.toString())) + .map((i) => ({ + code: 'invalid_intersection_types', + message: `Add '${i.toString()}' to query param 'include'`, + path: ['fields'], + })); + + errors.push(...fieldsErrors); + } + + if (sort) { + const { target: targetResourceSorts, ...relationSorts } = sort; + const selectRelationFields = ObjectTyped.keys(relationSorts); + const fieldsErrors = selectRelationFields + .filter((i) => !includeSet.has(i.toString())) + .map((i) => ({ + code: 'invalid_intersection_types', + message: `Add '${i.toString()}' to query param 'include'`, + path: ['sort'], + })); + + errors.push(...fieldsErrors); + } + + if (errors.length > 0) { + throw new BadRequestException(errors); + } + + return value; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts new file mode 100644 index 00000000..7422bf5a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/index.ts @@ -0,0 +1 @@ +export * from './query-input.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts new file mode 100644 index 00000000..c35bebab --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.spec.ts @@ -0,0 +1,65 @@ +import { QueryInputPipe } from './query-input.pipe'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ZodError } from 'zod'; +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; + +import { ZOD_INPUT_QUERY_SCHEMA } from '../../../../constants'; + +class MockZodInputQuery { + parse(value: unknown, options?: unknown) { + return value; + } +} + +describe('QueryInputPipe', () => { + let pipe: QueryInputPipe; + let zodSchemaMock: MockZodInputQuery; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QueryInputPipe, + { provide: ZOD_INPUT_QUERY_SCHEMA, useClass: MockZodInputQuery }, + ], + }).compile(); + + pipe = module.get>(QueryInputPipe); + zodSchemaMock = module.get(ZOD_INPUT_QUERY_SCHEMA); + }); + + it('should parse the input successfully', () => { + const input = { key: 'value' }; + jest.spyOn(zodSchemaMock, 'parse').mockReturnValue(input); + + const result = pipe.transform(input); + expect(result).toBe(input); + expect(zodSchemaMock.parse).toHaveBeenCalledWith(input, { + errorMap: expect.any(Function), + }); + }); + + it('should throw a BadRequestException when ZodError occurs', () => { + const input = { invalid: 'data' }; + const mockZodError = new ZodError([]); + + jest.spyOn(zodSchemaMock, 'parse').mockImplementation(() => { + throw mockZodError; + }); + + expect(() => pipe.transform(input)).toThrow(BadRequestException); + }); + + it('should throw an InternalServerErrorException for non-ZodError exceptions', () => { + const input = { key: 'value' }; + const mockError = new Error('Unexpected error'); + + jest.spyOn(zodSchemaMock, 'parse').mockImplementation(() => { + throw mockError; + }); + + expect(() => pipe.transform(input)).toThrow(InternalServerErrorException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts new file mode 100644 index 00000000..c1129549 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-input/query-input.pipe.ts @@ -0,0 +1,33 @@ +import { + Inject, + PipeTransform, + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { ZodError } from 'zod'; +import { errorMap } from 'zod-validation-error'; +import { ZOD_INPUT_QUERY_SCHEMA } from '../../../../constants'; +import { ZodInputQuery, InputQuery } from '../../zod'; +import { JSONValue } from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +export class QueryInputPipe + implements PipeTransform> +{ + @Inject(ZOD_INPUT_QUERY_SCHEMA) + private zodInputQuerySchema!: ZodInputQuery; + + transform(value: JSONValue): InputQuery { + try { + return this.zodInputQuerySchema.parse(value, { + errorMap: errorMap, + }); + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts new file mode 100644 index 00000000..a8853ae0 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/index.ts @@ -0,0 +1 @@ +export * from './query.pipe'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts new file mode 100644 index 00000000..cfb58c24 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts @@ -0,0 +1,107 @@ +import { + BadRequestException, + InternalServerErrorException, +} from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ZodError } from 'zod'; + +import { QueryPipe } from './query.pipe'; +import { ASC, ZOD_QUERY_SCHEMA } from '../../../../constants'; +import { ZodQuery, InputQuery, Query } from '../../zod'; +import { FilterOperand, QueryField } from '@klerick/json-api-nestjs-shared'; + +type MockEntity = { id: number; name: string }; + +describe('QueryPipe', () => { + let queryPipe: QueryPipe; + let zodQuerySchemaMock: ZodQuery; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QueryPipe, + { + provide: ZOD_QUERY_SCHEMA, + useValue: { + parse: jest.fn(), + }, + }, + ], + }).compile(); + + queryPipe = module.get>(QueryPipe); + zodQuerySchemaMock = module.get>(ZOD_QUERY_SCHEMA); + }); + + it('should parse the query successfully using the zod schema', () => { + const inputQuery: InputQuery = { + [QueryField.fields]: { + target: ['id', 'name'], + }, + [QueryField.filter]: { + relation: null, + target: { + id: { + [FilterOperand.eq]: '1', + }, + }, + }, + [QueryField.include]: null, + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + const parsedQuery: Query = { + [QueryField.fields]: { + target: ['id', 'name'], + }, + [QueryField.filter]: { + relation: null, + target: { + id: { + eq: '1', + }, + }, + }, + [QueryField.include]: null, + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + + jest.spyOn(zodQuerySchemaMock, 'parse').mockReturnValue(parsedQuery); + + const result = queryPipe.transform(inputQuery); + + expect(result).toEqual(parsedQuery); + expect(zodQuerySchemaMock.parse).toHaveBeenCalledWith(inputQuery); + }); + + it('should throw BadRequestException if ZodError is thrown', () => { + const inputQuery = { + id: 1, + name: 'Invalid', + } as unknown as InputQuery; + const zodError = new ZodError([]); + + jest.spyOn(zodQuerySchemaMock, 'parse').mockImplementation(() => { + throw zodError; + }); + + expect(() => queryPipe.transform(inputQuery)).toThrow(BadRequestException); + }); + + it('should throw InternalServerErrorException if an unknown error is thrown', () => { + const inputQuery = { + id: 1, + name: 'Invalid', + } as unknown as InputQuery; + const unexpectedError = new Error('Unexpected error'); + + jest.spyOn(zodQuerySchemaMock, 'parse').mockImplementation(() => { + throw unexpectedError; + }); + + expect(() => queryPipe.transform(inputQuery)).toThrow( + InternalServerErrorException + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts new file mode 100644 index 00000000..d4c5d359 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts @@ -0,0 +1,30 @@ +import { + InternalServerErrorException, + BadRequestException, + Inject, + PipeTransform, +} from '@nestjs/common'; +import { ZodError } from 'zod'; + +import { ZOD_QUERY_SCHEMA } from '../../../../constants'; +import { ZodQuery, Query, InputQuery } from '../../zod'; +import { ObjectLiteral } from '../../../../types'; + +export class QueryPipe + implements PipeTransform, Query> +{ + @Inject(ZOD_QUERY_SCHEMA) + private zodQuerySchema!: ZodQuery; + + transform(value: InputQuery): Query { + try { + return this.zodQuerySchema.parse(value); + } catch (e) { + if (e instanceof ZodError) { + throw new BadRequestException(e.issues); + } + + throw new InternalServerErrorException(e); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts new file mode 100644 index 00000000..eebff2fc --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/binding.types.ts @@ -0,0 +1,47 @@ +import { PipeTransform, RequestMethod } from '@nestjs/common'; +import { Type } from '@nestjs/common/interfaces'; +import { PipeFabric } from './module.types'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { ObjectLiteral } from '../../../types'; + +export type MethodName = + | 'getAll' + | 'getOne' + | 'postOne' + | 'patchOne' + | 'getRelationship' + | 'deleteOne' + | 'deleteRelationship' + | 'postRelationship' + | 'patchRelationship'; + +type MapNameToTypeMethod = { + getAll: RequestMethod.GET; + getOne: RequestMethod.GET; + patchOne: RequestMethod.PATCH; + patchRelationship: RequestMethod.PATCH; + postOne: RequestMethod.POST; + postRelationship: RequestMethod.POST; + deleteOne: RequestMethod.DELETE; + deleteRelationship: RequestMethod.DELETE; + getRelationship: RequestMethod.GET; +}; + +export interface Binding { + path: string; + method: MapNameToTypeMethod[T]; + name: T; + implementation: JsonBaseController[T]; + parameters: { + decorator: ( + property?: string, + ...pipes: (Type | PipeTransform)[] + ) => ParameterDecorator; + property?: string; + mixins: PipeFabric[]; + }[]; +} + +export type BindingsConfig = { + [Key in MethodName]: Binding; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts new file mode 100644 index 00000000..4911b5dd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/decorator-options.types.ts @@ -0,0 +1,11 @@ +import { MethodName } from './binding.types'; + +import { RequiredFromPartial, ConfigParam } from '../../../types'; +import { MicroOrmParam } from '../../micro-orm'; +import { TypeOrmParam } from '../../type-orm'; + +export type DecoratorOptions = Partial< + { + allowMethod: Array; + } & RequiredFromPartial +>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts new file mode 100644 index 00000000..d3876db6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/index.ts @@ -0,0 +1,6 @@ +export * from './module.types'; +export * from './decorator-options.types'; +export * from './binding.types'; +export * from './utils'; +export * from './zod-types'; +export * from './orm-service.type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts new file mode 100644 index 00000000..768387c6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/module.types.ts @@ -0,0 +1,28 @@ +import { + AnyEntity, + EntityName, + NestImport, + NestController, + RequiredFromPartial, + ConfigParam, + PipeMixin, + ExtractNestType, + ResultModuleOptions, +} from '../../../types'; +import { MicroOrmParam } from '../../micro-orm'; +import { TypeOrmParam } from '../../type-orm'; + +type Controller = ExtractNestType; + +export interface MixinOptions { + entity: EntityName; + controller: Controller | undefined; + config: RequiredFromPartial; + imports: NestImport; + ormModule: ResultModuleOptions['type']; +} + +export type PipeFabric = >( + entity: Entity, + config?: MixinOptions['config'] +) => PipeMixin; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts new file mode 100644 index 00000000..b6a501ac --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts @@ -0,0 +1,55 @@ +import { EntityTarget, ObjectLiteral } from '../../../types'; + +import { + EntityRelation, + ResourceObject, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../zod'; + +export interface OrmService { + getAll(query: Query): Promise>; + getOne(id: number | string, query: QueryOne): Promise>; + deleteOne(id: number | string): Promise; + postOne(inputData: PostData): Promise>; + patchOne( + id: number | string, + inputData: PatchData + ): Promise>; + getRelationship>( + id: number | string, + rel: Rel + ): Promise>; + postRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise>; + deleteRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise; + patchRelationship>( + id: number | string, + rel: Rel, + input: PatchRelationshipData + ): Promise>; +} + +export type FindOneRowEntity = ( + entity: EntityTarget, + params: number | string +) => Promise; + +export type CheckRelationNme = ( + entity: EntityTarget, + params: string +) => boolean; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts new file mode 100644 index 00000000..821a02fb --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts @@ -0,0 +1,68 @@ +import { Type } from '@nestjs/common/interfaces'; + +import { + EntityField, + EntityRelation, + TypeOfArray, + EntityProps, +} from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral as Entity } from '../../../types'; + +export { EntityField, EntityProps, EntityRelation, TypeOfArray }; + +export type EntityPropsArray = { + [P in keyof T]: T[P] extends EntityField + ? IsArray extends true + ? P + : never + : never; +}[keyof T]; + +type UnionToIntersection = ( + U extends never ? never : (arg: U) => never +) extends (arg: infer I) => void + ? I + : never; + +export type UnionToTupleMain = UnionToIntersection< + T extends never ? never : (t: T) => T +> extends (_: never) => infer W + ? UnionToTupleMain, [...A, W]> + : A; + +export type UnionToTuple = UnionToTupleMain extends readonly [ + string, + ...string[] +] + ? UnionToTupleMain + : never; + +export type TypeCast = A extends T ? A : never; + +export type CastProps = K extends keyof E ? E[K] : never; + +export type TypeFromType = T extends Type ? A : never; + +export type ConcatStringArray = T extends [ + infer F extends string, + ...infer R extends string[] +] + ? `${F}${ConcatStringArray}` + : ''; + +export type Concat = ConcatStringArray< + [E, '.', F] +>; + +export type ValueOf = T[keyof T]; + +export type JSONValue = + | string + | number + | boolean + | null + | { [x: string]: JSONValue } + | Array; + +export type IsArray = [Extract] extends [never] ? false : true; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts new file mode 100644 index 00000000..cf413985 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts @@ -0,0 +1,141 @@ +import { Type } from '@nestjs/common'; +import { z } from 'zod'; + +import { + EntityProps, + EntityRelation, + UnionToTuple, + CastProps, + TypeOfArray, + EntityPropsArray, + IsArray, +} from './utils'; +import { + EntityTarget, + ObjectLiteral as Entity, + ObjectLiteral, +} from '../../../types'; + +export enum PropsNameResultField { + field = 'field', + relations = 'relations', +} + +export type ResultGetField = { + [PropsNameResultField.field]: TupleOfEntityProps; + [PropsNameResultField.relations]: TupleOfEntityRelation; +}; + +export type TupleOfEntityProps< + E, + Props = UnionToTuple> +> = Props extends readonly [string, ...string[]] ? Props : never; +export type TupleOfEntityRelation< + E, + Props = UnionToTuple> +> = Props extends readonly [string, ...string[]] ? Props : never; + +export type RelationTree = { + [K in keyof RelationType]: TypeOfArray extends Entity + ? ResultGetField>['field'] + : never; +}; + +export type RelationType = { + [K in EntityRelation]: Type>>; +}; + +export type ZodInfer any> = z.infer>; + +export type GetFieldForEntity = ( + entity: EntityTarget +) => ResultGetField; + +export type ZodParams< + E extends Entity, + P extends EntityProps, + I = string +> = { + entityFieldsStructure: ResultGetField; + entityRelationStructure: RelationTree; + propsArray: ArrayPropsForEntity; + propsType: AllFieldWithType; + typeId: TypeForId; + typeName: I; + fieldWithType: FieldWithType; + propsDb: PropsForField; + primaryColumn: P; + relationArrayProps: RelationPropsArray; + relationPopsName: RelationPropsTypeName; + primaryColumnType: RelationPrimaryColumnType; +}; + +export type PropsArray = { [K in EntityPropsArray]: true }; + +export type ArrayPropsForEntity = { + target: PropsArray; +} & { + [K in ResultGetField['relations'][number]]: PropsArray< + TypeOfArray> + >; +}; + +export enum TypeField { + array = 'array', + date = 'date', + number = 'number', + boolean = 'boolean', + string = 'string', + object = 'object', +} +export type TypeForId = Extract; + +export type FieldWithType = { + [K in EntityProps]: IsArray extends true + ? TypeField.array + : E[K] extends Date + ? TypeField.date + : E[K] extends number + ? TypeField.number + : E[K] extends boolean + ? TypeField.boolean + : E[K] extends object + ? TypeField.object + : TypeField.string; +}; + +export type AllFieldWithType = FieldWithType & { + [K in EntityRelation]: E[K] extends (infer U extends Entity)[] + ? FieldWithType + : E[K] extends Entity + ? FieldWithType + : never; +}; + +export type PropsForField = { + [K in EntityProps]: PropsFieldItem; +}; + +export type ColumnType = + | T + | typeof Number + | typeof Date + | typeof Boolean; + +export type PropsFieldItem = { + type: ColumnType; + isArray: boolean; + isNullable: boolean; +}; + +export type RelationPropsArray = { + [K in EntityRelation]: E[K] extends unknown[] ? true : false; +}; + +export type RelationPropsTypeName = { + [K in EntityRelation]: string; +}; + +export type RelationPrimaryColumnType = { + [K in EntityRelation]: TypeForId; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts new file mode 100644 index 00000000..6d2793fd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts @@ -0,0 +1,6 @@ +export * from './zod-input-query-schema'; +export * from './zod-query-schema'; +export * from './zod-input-post-schema'; +export * from './zod-input-patch-schema'; +export * from './zod-input-post-relationship-schema'; +export * from './zod-input-patch-relationship-schema'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts new file mode 100644 index 00000000..0328c8be --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.spec.ts @@ -0,0 +1,42 @@ +import { zodPatchRelationship } from './index'; + +describe('zodPatchRelationship', () => { + const schema = zodPatchRelationship; + + it('should validate an object with nullable data matching zodData', () => { + const validData = { data: { id: '123', type: 'example' } }; + + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with null as the value of data', () => { + const validData = { data: null }; + + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with empty array as the value of data', () => { + const validData = { data: [] }; + + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with an array of objects matching zodData', () => { + const validData = { + data: [ + { id: '123', type: 'example' }, + { id: '456', type: 'example2' }, + ], + }; + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should throw an error for extra unknown properties', () => { + const invalidData = { + data: { id: '123', type: 'example' }, + extra: 'invalid', + }; + + expect(() => schema.parse(invalidData)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts new file mode 100644 index 00000000..de334b2d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-relationship-schema/index.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +import { zodData } from '../zod-share'; + +export const zodPatchRelationship = z + .object({ + data: z.union([zodData().nullable(), zodData().array()]), + }) + .strict(); + +export type ZodPatchRelationship = typeof zodPatchRelationship; +export type PatchRelationship = z.infer; +export type PatchRelationshipData = PatchRelationship['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts new file mode 100644 index 00000000..b86972dd --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts @@ -0,0 +1,220 @@ +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeField, + TypeForId, +} from '../../types'; +import { Users } from '../../../../mock-utils'; +import { zodPatch, PatchData } from './'; +import { ZodError } from 'zod'; + +const typeId: TypeForId = TypeField.number; +const typeName = 'Users'; +const fieldWithType: FieldWithType = { + id: TypeField.number, + login: TypeField.string, + firstName: TypeField.string, + testReal: TypeField.array, + testArrayNull: TypeField.array, + lastName: TypeField.string, + isActive: TypeField.boolean, + createdAt: TypeField.date, + testDate: TypeField.date, + updatedAt: TypeField.date, +}; +const propsDb: PropsForField = { + id: { type: 'number', isArray: false, isNullable: false }, + login: { type: 'string', isArray: false, isNullable: false }, + firstName: { type: 'string', isArray: false, isNullable: true }, + testReal: { type: 'number', isArray: true, isNullable: false }, + testArrayNull: { type: 'number', isArray: true, isNullable: true }, + lastName: { type: 'string', isArray: false, isNullable: true }, + isActive: { type: 'boolean', isArray: false, isNullable: true }, + createdAt: { type: 'date', isArray: false, isNullable: true }, + testDate: { type: 'date', isArray: false, isNullable: true }, + updatedAt: { type: 'date', isArray: false, isNullable: true }, +}; +const primaryColumn: EntityProps = 'id'; +const relationArrayProps: RelationPropsArray = { + roles: true, + comments: true, + notes: true, + addresses: false, + userGroup: false, + manager: false, +}; +const relationPopsName: RelationPropsTypeName = { + roles: 'Roles', + comments: 'Comments', + notes: 'Notes', + addresses: 'Addresses', + userGroup: 'UserGroups', + manager: 'Users', +}; +const primaryColumnType: RelationPrimaryColumnType = { + roles: TypeField.number, + userGroup: TypeField.number, + manager: TypeField.number, + addresses: TypeField.number, + comments: TypeField.number, + notes: TypeField.string, +}; +const schema = zodPatch( + typeId, + 'users', + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType +); + +describe('zodPatch', () => { + it('should be ok', () => { + const real = 123.123; + const date = new Date(); + const attributes = { + login: 'login', + testDate: date.toISOString(), + testReal: [`${real}`], + testArrayNull: null, + }; + const relationships = { + notes: { + data: [ + { + type: 'notes', + id: 'dsfsdf', + }, + ], + }, + }; + + const check = { + data: { + id: '1', + type: 'users', + attributes, + relationships, + }, + }; + const check2 = { + data: { + id: '1', + type: 'users', + attributes, + }, + }; + + const checkResult = { + data: { + id: '1', + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + relationships, + }, + }; + const checkResult2 = { + data: { + id: '1', + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + }, + }; + + const checkResult3 = { + data: { + id: '1', + type: 'users', + }, + }; + + expect(schema.parse(check)).toEqual(checkResult); + expect(schema.parse(check2)).toEqual(checkResult2); + expect(schema.parse(checkResult3)).toEqual(checkResult3); + }); + it('should be not ok', () => { + const check1 = {}; + const check2 = null; + const check3: unknown[] = []; + const check4 = ''; + const check5 = { + sdf: 'sdf', + }; + const check6 = { + data: {}, + }; + const check7 = { + data: { + type: 'users', + }, + }; + const check8 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + isActive: true, + }, + relationships: { + notes: [ + { + type: 'sdfsdf', + id: 'dsfsdf', + }, + ], + }, + }, + }; + const check9 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + id: 1, + }, + }, + }; + const check10 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + }, + }, + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + check10, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts new file mode 100644 index 00000000..b1444e11 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.ts @@ -0,0 +1,111 @@ +import { z, ZodObject, ZodOptional } from 'zod'; +import { + zodAttributes, + ZodAttributes, + zodId, + ZodId, + zodRelationships, + ZodRelationships, + zodType, + ZodType, +} from '../zod-share'; +import { ObjectLiteral } from '../../../../types'; +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeForId, +} from '../../types'; +import { ZodPost } from '../zod-input-post-schema'; + +type ZodPatchPatchShape = { + id: ZodId; + type: ZodType; + attributes: ZodOptional>; + relationships: ZodOptional>; +}; + +type ZodInputPatchSchema = ZodObject< + ZodPatchPatchShape, + 'strict' +>; + +type ZodInputPatchDataShape = { + data: ZodInputPatchSchema; +}; + +function getShape( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodInputPatchSchema { + const shape = { + id: zodId(typeId), + type: zodType(typeName), + attributes: zodAttributes( + fieldWithType, + propsDb, + primaryColumn, + true + ).optional(), + relationships: zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + true + ).optional(), + }; + + return z.object(shape).strict(); +} + +function zodDataShape( + shape: ZodInputPatchSchema +): ZodPatch { + return z + .object({ + data: shape, + }) + .strict(); +} + +export function zodPatch( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodPatch { + const shape = getShape( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + + return zodDataShape(shape); +} + +export type ZodPatch = ZodObject< + ZodInputPatchDataShape, + 'strict' +>; +export type PatchData< + E extends ObjectLiteral, + N extends string = string +> = z.infer>['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts new file mode 100644 index 00000000..9b084d58 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.spec.ts @@ -0,0 +1,43 @@ +import { zodPostRelationship } from './index'; + +describe('zodPostRelationship', () => { + const schema = zodPostRelationship; + + it('should validate an object with a single valid data item', () => { + const validData = { data: { id: '1', type: 'example' } }; + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should validate an object with a non-empty array of valid data items', () => { + const validData = { + data: [ + { id: '1', type: 'example1' }, + { id: '2', type: 'example2' }, + ], + }; + expect(() => schema.parse(validData)).not.toThrow(); + }); + + it('should throw an error when data is an empty array', () => { + const invalidData = { data: [] }; + expect(() => schema.parse(invalidData)).toThrow(); + }); + + it('should throw an error when data is null', () => { + const invalidData = { data: null }; + expect(() => schema.parse(invalidData)).toThrow(); + }); + + it('should throw an error when data is missing', () => { + const invalidData = {}; + expect(() => schema.parse(invalidData)).toThrow(); + }); + + it('should throw an error when additional properties are included', () => { + const invalidData = { + data: { id: '1', type: 'example' }, + extra: 'invalid', + }; + expect(() => schema.parse(invalidData)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts new file mode 100644 index 00000000..d0c245b9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-relationship-schema/index.ts @@ -0,0 +1,13 @@ +import { z } from 'zod'; + +import { zodData } from '../zod-share'; + +export const zodPostRelationship = z + .object({ + data: z.union([zodData(), zodData().array().nonempty()]), + }) + .strict(); + +export type ZodPostRelationship = typeof zodPostRelationship; +export type PostRelationship = z.infer; +export type PostRelationshipData = PostRelationship['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts new file mode 100644 index 00000000..61bb555b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts @@ -0,0 +1,219 @@ +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeField, + TypeForId, +} from '../../types'; +import { Users } from '../../../../mock-utils'; +import { zodPost, PostData } from './'; +import { ZodError } from 'zod'; + +const typeId: TypeForId = TypeField.number; +const typeName = 'Users'; +const fieldWithType: FieldWithType = { + id: TypeField.number, + login: TypeField.string, + firstName: TypeField.string, + testReal: TypeField.array, + testArrayNull: TypeField.array, + lastName: TypeField.string, + isActive: TypeField.boolean, + createdAt: TypeField.date, + testDate: TypeField.date, + updatedAt: TypeField.date, +}; +const propsDb: PropsForField = { + id: { type: 'number', isArray: false, isNullable: false }, + login: { type: 'string', isArray: false, isNullable: false }, + firstName: { type: 'string', isArray: false, isNullable: true }, + testReal: { type: 'number', isArray: true, isNullable: false }, + testArrayNull: { type: 'number', isArray: true, isNullable: true }, + lastName: { type: 'string', isArray: false, isNullable: true }, + isActive: { type: 'boolean', isArray: false, isNullable: true }, + createdAt: { type: 'date', isArray: false, isNullable: true }, + testDate: { type: 'date', isArray: false, isNullable: true }, + updatedAt: { type: 'date', isArray: false, isNullable: true }, +}; +const primaryColumn: EntityProps = 'id'; +const relationArrayProps: RelationPropsArray = { + roles: true, + comments: true, + notes: true, + addresses: false, + userGroup: false, + manager: false, +}; +const relationPopsName: RelationPropsTypeName = { + roles: 'Roles', + comments: 'Comments', + notes: 'Notes', + addresses: 'Addresses', + userGroup: 'UserGroups', + manager: 'Users', +}; +const primaryColumnType: RelationPrimaryColumnType = { + roles: TypeField.number, + userGroup: TypeField.number, + manager: TypeField.number, + addresses: TypeField.number, + comments: TypeField.number, + notes: TypeField.string, +}; +const schema = zodPost( + typeId, + 'users', + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType +); + +describe('zodPost', () => { + it('should be ok', () => { + const real = 123.123; + const date = new Date(); + const attributes = { + login: 'login', + lastName: 'sdfsdf', + isActive: true, + testDate: date.toISOString(), + testReal: [`${real}`], + testArrayNull: null, + }; + const relationships = { + notes: { + data: [ + { + type: 'notes', + id: 'dsfsdf', + }, + ], + }, + }; + const check = { + data: { + type: 'users', + attributes, + relationships, + }, + }; + const check2 = { + data: { + type: 'users', + attributes, + }, + }; + const check3 = { + data: { + id: '1', + type: 'users', + attributes, + }, + }; + + const checkResult = { + data: { + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + relationships, + }, + }; + const checkResult2 = { + data: { + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + }, + }; + const checkResult3 = { + data: { + id: '1', + type: 'users', + attributes: { + ...attributes, + ['testDate']: date, + testReal: [real], + }, + }, + }; + + expect(schema.parse(check)).toEqual(checkResult); + expect(schema.parse(check2)).toEqual(checkResult2); + expect(schema.parse(check3)).toEqual(checkResult3); + }); + it('should be not ok', () => { + const check1 = {}; + const check2 = null; + const check3: unknown[] = []; + const check4 = ''; + const check5 = { + sdf: 'sdf', + }; + const check6 = { + data: {}, + }; + const check7 = { + data: { + type: 'users', + }, + }; + const check8 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + isActive: true, + }, + relationships: { + notes: [ + { + type: 'sdfsdf', + id: 'dsfsdf', + }, + ], + }, + }, + }; + const check9 = { + data: { + type: 'users', + attributes: { + lastName: 'sdfsdf', + id: 1, + }, + }, + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + schema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts new file mode 100644 index 00000000..74302679 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.ts @@ -0,0 +1,109 @@ +import { z, ZodObject, ZodOptional } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { + ZodId, + zodId, + ZodType, + zodType, + ZodAttributes, + zodAttributes, + ZodRelationships, + zodRelationships, +} from '../zod-share'; +import { + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + TypeForId, +} from '../../types'; + +type ZodInputPostShape = { + id: ZodOptional; + type: ZodType; + attributes: ZodAttributes; + relationships: ZodOptional>; +}; + +type ZodInputPostSchema = ZodObject< + ZodInputPostShape, + 'strict' +>; + +type ZodInputPostDataShape = { + data: ZodInputPostSchema; +}; + +function getShape( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodInputPostSchema { + const shape = { + id: zodId(typeId).optional(), + type: zodType(typeName), + attributes: zodAttributes(fieldWithType, propsDb, primaryColumn, false), + relationships: zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + false + ).optional(), + }; + + return z.object(shape).strict(); +} + +function zodDataShape( + shape: ZodInputPostSchema +): ZodPost { + return z + .object({ + data: shape, + }) + .strict(); +} + +export function zodPost( + typeId: TypeForId, + typeName: N, + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType +): ZodPost { + const shape = getShape( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ); + + return zodDataShape(shape); +} + +export type ZodPost = ZodObject< + ZodInputPostDataShape, + 'strict' +>; +export type Post = z.infer< + ZodPost +>; +export type PostData = Post< + E, + N +>['data']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts new file mode 100644 index 00000000..3486cbae --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts @@ -0,0 +1,111 @@ +import { zodFieldsInputQuery } from './fields'; +import { ResultGetField } from '../../types'; +import { Users } from '../../../../mock-utils'; + +describe('zodFieldsInputQuerySchema', () => { + const validRelationList: ResultGetField['relations'] = [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ]; + const schema = zodFieldsInputQuery(validRelationList); + + it('should validate successfully with a valid target and relation', () => { + const targetInput = 'field1,field2'; + const commentsInput = 'text,createdAt'; + const input = { + target: targetInput, + roles: '', + comments: commentsInput, + }; + const result = schema.safeParse(input); + expect.assertions(4); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveProperty('target', targetInput.split(',')); + expect(result.data).toHaveProperty('comments', commentsInput.split(',')); + expect(result.data).not.toHaveProperty('roles'); + } + }); + + it('should be null result', () => { + const input = { + target: '', + }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toBe(null); + } + }); + + it('should throw error if target is missing', () => { + const input = { + posts: ['content'], + }; + + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Validation error: Fields should be have only props' + ); + } + }); + + it('Not allow null', () => { + const input = { + target: 'inputString', + comments: null, + }; + + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Expected string, received null' + ); + } + }); + + it('should return null if all fields in input are empty or null', () => { + const input = { + target: undefined, + }; + + expect(schema.parse(input)).toBeNull(); + }); + + it('should throw error if additional fields are present in the input', () => { + const input = { + target: null, + notes: false, + comments: 1, + addresses: ['invalidValue'], // Invalid field + }; + + const result = schema.safeParse(input); + expect.assertions(5); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Expected string, received null' + ); + expect(result.error.issues[1].message).toContain( + 'Expected string, received boolean' + ); + expect(result.error.issues[2].message).toContain( + 'Expected string, received number' + ); + expect(result.error.issues[3].message).toContain( + 'Expected string, received array' + ); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts new file mode 100644 index 00000000..9bd95c11 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts @@ -0,0 +1,59 @@ +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { ResultGetField, ZodInfer } from '../../types'; +import { nonEmptyObject, getValidationErrorForStrict } from '../zod-utils'; + +function getZodRules() { + return z + .string() + .optional() + .transform((input) => (input ? input.split(',') : undefined)); +} + +type ZodRule = ReturnType; + +export function zodFieldsInputQuery( + relationList: ResultGetField['relations'] +) { + const target = z.object({ + target: getZodRules(), + }); + + const relation = relationList.reduce( + (acum, item) => ({ + ...acum, + [item]: getZodRules(), + }), + {} as { + [K in ResultGetField['relations'][number]]: ZodRule; + } + ); + + return target + .merge(z.object(relation)) + .strict(getValidationErrorForStrict(['target', ...relationList], 'Fields')) + .refine(nonEmptyObject(), { + message: 'Validation error: Need select field for target or relation', + }) + .optional() + .transform((input) => { + if (!input) return null; + + const result = ObjectTyped.entries(input).reduce((acum, [key, value]) => { + if (!value || value.length === 0) return acum; + + return { + ...acum, + [key]: value, + }; + }, {} as typeof input); + + return Object.keys(result).length > 0 ? result : null; + }); +} + +export type ZodFieldsInputQuery = ZodInfer< + typeof zodFieldsInputQuery +>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts new file mode 100644 index 00000000..919c41df --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts @@ -0,0 +1,123 @@ +import { zodFilterInputQuery } from './filter'; +import { Users } from '../../../../mock-utils'; +import { RelationTree, ResultGetField } from '../../types'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +describe('zodFilterInputQuery', () => { + it('should return transformed result with relation and target when valid data is provided', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + const input = { + login: { eq: 'johndoe' }, + addresses: { eq: 'null' }, + manager: { eq: null }, + userGroup: { ne: null }, + 'addresses.city': { eq: 'New York' }, + }; + + const result = schema.parse(input); + + expect(result).toEqual({ + relation: { + addresses: { city: { eq: 'New York' } }, + }, + target: { + addresses: { eq: null }, + manager: { eq: null }, + login: { eq: 'johndoe' }, + userGroup: { ne: null }, + }, + }); + }); + + it('should return null relation and target when no data is provided', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + + const result = schema.parse({}); + expect(result).toEqual({ relation: null, target: null }); + }); + + it('should ignore invalid fields and not include them in the result', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + const input = { + invalidField: { eq: 'should be ignored' }, + login: { eq: 'johndoe' }, + }; + + const result = schema.parse(input); + + expect(result).toEqual({ + relation: null, + target: { login: { eq: 'johndoe' } }, + }); + }); + + it('should handle nested relations correctly', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + const input = { + 'manager.firstName': { like: 'Jane' }, + 'manager.lastName': { nin: 'Doe,Jim' }, + }; + + const result = schema.parse(input); + + expect(result).toEqual({ + relation: { + manager: { + firstName: { like: 'Jane' }, + lastName: { nin: ['Doe', 'Jim'] }, + }, + }, + target: null, + }); + }); + + it('should throw a validation error for invalid structures', () => { + const schema = zodFilterInputQuery(userFields, userRelations); + + const invalidInput = { + login: { unknownOperator: 'invalid' }, + }; + + expect(() => schema.parse(invalidInput)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts new file mode 100644 index 00000000..9586fcbb --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts @@ -0,0 +1,196 @@ +import { + FilterOperand, + ObjectTyped, + isString, +} from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; + +import { + RelationTree, + ResultGetField, + UnionToTuple, + ZodInfer, +} from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +import { oneOf, stringLongerThan } from '../zod-utils'; + +const arrayOp = { + [FilterOperand.in]: true, + [FilterOperand.nin]: true, + [FilterOperand.some]: true, +}; +function convertToFilterObject( + value: Record | string +): Partial<{ + [key in FilterOperand]: string | string[]; +}> { + if (isString | string, string>(value)) { + return { + [FilterOperand.eq]: value, + }; + } else { + return Object.entries(value).reduce((acum, [op, filed]) => { + if (op in arrayOp) { + acum[op] = (isString(filed) ? filed.split(',') : []).filter((i) => !!i); + } else { + acum[op] = filed; + } + return acum; + }, {} as Record); + } +} + +type FilterType = Partial<{ + [key in FilterOperand]: string | string[]; +}>; + +type OutPutFilter = { + relation: null | Record>; + target: null | Record; +}; + +function getZodRulesForRelation() { + return z + .union([ + z + .object({ + [FilterOperand.eq]: z + .union([z.literal('null'), z.null()]) + .transform(() => null), + }) + .strict(), + z + .object({ + [FilterOperand.ne]: z + .union([z.literal('null'), z.null()]) + .transform(() => null), + }) + .strict(), + ]) + .optional(); +} + +function getZodRulesForFilterOperator() { + const filterConditional = z + .union([z.string().refine(stringLongerThan()), z.null(), z.number()]) + .transform((r) => `${r}`); + + const conditional = z + .object( + ObjectTyped.values(FilterOperand).reduce((acum, item) => { + acum[item] = filterConditional; + return acum; + }, {} as Record) + ) + .strict() + .partial() + .refine(oneOf(Object.values(FilterOperand)), { + message: `Must have one of: "${Object.values(FilterOperand).join( + '","' + )}"`, + }); + + return z.union([filterConditional, conditional]).optional(); +} + +function shapeForArray< + R extends readonly [string, ...string[]], + Z extends ZodRulesForRelation | ZodRulesForFilterOperator +>(fields: R, zodSchema: Z) { + return fields.reduce( + (acum, item) => ({ + ...acum, + [item]: zodSchema, + }), + {} as { + [K in R[number]]: Z; + } + ); +} + +function getTupleConcatRelationFields( + relationList: RelationTree +): UnionToTuple< + { + [K in keyof RelationTree]: `${K & string}.${RelationTree[K][number]}`; + }[keyof RelationTree] +> { + const result: string[] = []; + + for (const [key, val] of ObjectTyped.entries(relationList)) { + const relName = key.toString(); + for (const v of val) { + result.push(`${relName}.${v}`); + } + } + + return result as any; +} + +type ZodRulesForRelation = ReturnType; +type ZodRulesForFilterOperator = ReturnType< + typeof getZodRulesForFilterOperator +>; + +export function zodFilterInputQuery( + fields: ResultGetField['field'], + relationList: RelationTree +) { + const target = z.object( + shapeForArray(fields, getZodRulesForFilterOperator()) + ); + + const relationTuple = ObjectTyped.keys(relationList) as UnionToTuple< + keyof RelationTree + >; + + const relations = z.object( + shapeForArray(relationTuple, getZodRulesForRelation()) + ); + + const relationFields = z.object( + shapeForArray( + getTupleConcatRelationFields(relationList), + getZodRulesForFilterOperator() + ) + ); + + return target + .merge(relations) + .merge(relationFields) + .optional() + .transform((data) => { + if (!data) { + return { + relation: null, + target: null, + }; + } + return Object.entries(data).reduce( + (acum, [field, value]: [string, any]) => { + const objectOperand = convertToFilterObject(value); + if (Object.keys(objectOperand).length === 0) { + return acum; + } + + if (field.indexOf('.') > -1) { + const [relation, fieldRelation] = field.split('.'); + acum['relation'] = !acum['relation'] ? {} : acum['relation']; + acum['relation'][relation] = acum['relation'][relation] || {}; + acum['relation'][relation][fieldRelation] = objectOperand; + } else { + acum['target'] = !acum['target'] ? {} : acum['target']; + acum['target'][field] = objectOperand; + } + + return acum; + }, + { relation: null, target: null } as OutPutFilter + ); + }); +} + +export type ZodFilterInputQuery = ZodInfer< + typeof zodFilterInputQuery +>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts new file mode 100644 index 00000000..3a202259 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.spec.ts @@ -0,0 +1,32 @@ +import { zodIncludeInputQuery } from './include'; + +describe('zodIncludeInputQuerySchema', () => { + const schema = zodIncludeInputQuery(); + + it('should return null when input is undefined', () => { + const result = schema.parse(undefined); + expect(result).toBeNull(); + }); + + it('should return null when input is number, null, boolean, array', () => { + expect(() => schema.parse(123 as any)).toThrow(); + expect(() => schema.parse(null as any)).toThrow(); + expect(() => schema.parse(false as any)).toThrow(); + expect(() => schema.parse([] as any)).toThrow(); + }); + + it('should split a comma-separated string into an array of trimmed values', () => { + const result = schema.parse('a, b ,c , d'); + expect(result).toEqual(['a', 'b', 'c', 'd']); + }); + + it('should filter out empty values from the resulting array', () => { + const result = schema.parse('a, , b, , c'); + expect(result).toEqual(['a', 'b', 'c']); + }); + + it('should return null for an empty string', () => { + const result = schema.parse(''); + expect(result).toBeNull(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts new file mode 100644 index 00000000..639d34c8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; +import { isString } from '@klerick/json-api-nestjs-shared'; +import { ZodInfer } from '../../types'; + +export function zodIncludeInputQuery() { + return z + .string() + .optional() + .transform((data) => { + if (!data || !isString(data)) return null; + return data + .split(',') + .map((i) => i.trim()) + .filter((i) => !!i); + }); +} + +export type ZodIncludeInputQuery = ZodInfer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts new file mode 100644 index 00000000..7873d08c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts @@ -0,0 +1,136 @@ +import { QueryField, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { zodInputQuery } from './index'; + +import { + RelationTree, + ResultGetField, + TupleOfEntityRelation, +} from '../../types'; +import { Users } from '../../../../mock-utils'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +const entityFieldsStructure = { + field: userFields, + relations: ObjectTyped.keys(userRelations) as TupleOfEntityRelation, +}; + +describe('zodInputQuery', () => { + it('should validate a correct input query object', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: { + target: 'login', + roles: 'name', + manager: 'firstName', + }, + [QueryField.filter]: { id: 1 }, + [QueryField.include]: 'manager,roles', + [QueryField.sort]: 'login', + [QueryField.page]: { number: 1, size: 10 }, + }; + try { + schema.parse(input); + } catch (e) { + console.log(e); + } + + expect(() => schema.parse(input)).not.toThrow(); + }); + + it('should throw an error for an invalid field in the input query', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: ['invalidRelation.invalidField'], + [QueryField.filter]: { id: 1 }, + [QueryField.include]: ['addresses'], + [QueryField.sort]: [{ field: 'login', order: 'asc' }], + [QueryField.page]: { number: 2, size: 5 }, + }; + + expect(() => schema.parse(input)).toThrow(); + }); + + it('should throw an error if an unexpected key is present in the input query', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: ['roles.name'], + [QueryField.filter]: { id: 1 }, + [QueryField.include]: ['roles'], + [QueryField.sort]: [{ field: 'login', order: 'asc' }], + [QueryField.page]: { number: 1, size: 10 }, + unexpectedKey: 'unexpectedValue', + }; + + expect(() => schema.parse(input)).toThrow(); + }); + + it('should throw an error when a required field is missing', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.fields]: ['roles.name'], + [QueryField.include]: ['roles'], + [QueryField.sort]: [{ field: 'login', order: 'asc' }], + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schema.parse(input)).toThrow(); + }); + + it('should validate input with empty but valid fields', () => { + const schema = zodInputQuery(entityFieldsStructure, userRelations); + const input = { + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schema.parse(input)).not.toThrow(); + const result = schema.parse(input); + + expect(result).toEqual({ + fields: null, + filter: { relation: null, target: null }, + include: null, + sort: null, + page: { size: 10, number: 1 }, + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts new file mode 100644 index 00000000..04e80722 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts @@ -0,0 +1,39 @@ +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; +import { RelationTree, ResultGetField } from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +import { zodFieldsInputQuery } from './fields'; +import { zodFilterInputQuery } from './filter'; +import { zodIncludeInputQuery } from './include'; +import { zodSortInputQuery } from './sort'; +import { zodPageInputQuery } from '../zod-share'; + +export function zodInputQuery( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree +) { + return z + .object({ + [QueryField.fields]: zodFieldsInputQuery( + entityFieldsStructure.relations + ), + [QueryField.filter]: zodFilterInputQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.include]: zodIncludeInputQuery(), + [QueryField.sort]: zodSortInputQuery(), + [QueryField.page]: zodPageInputQuery(), + }) + .strict( + `Query object should contain only allow params: "${Object.keys( + QueryField + ).join('"."')}"` + ); +} + +export type ZodInputQuery = ReturnType< + typeof zodInputQuery +>; +export type InputQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts new file mode 100644 index 00000000..682fa534 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.spec.ts @@ -0,0 +1,56 @@ +import { zodSortInputQuery } from './sort'; +import { ASC, DESC } from '../../../../constants'; + +describe('zodSortInputQuerySchema', () => { + const schema = zodSortInputQuery(); + + it('should transform a single field sort to the correct format', () => { + const result = schema.parse('name'); + expect(result).toEqual({ target: { name: ASC } }); + }); + + it('should transform a descending field sort to the correct format', () => { + const result = schema.parse('-name'); + expect(result).toEqual({ target: { name: DESC } }); + }); + + it('should transform multiple fields sort to the correct format', () => { + const result = schema.parse('name,-age'); + expect(result).toEqual({ target: { name: ASC, age: DESC } }); + }); + + it('should handle nested fields sort properly', () => { + const result = schema.parse('user.name,-user.age'); + expect(result).toEqual({ + user: { name: ASC, age: DESC }, + }); + }); + + it('should ignore empty or invalid inputs between commas', () => { + const result = schema.parse('name,,,-age'); + expect(result).toEqual({ target: { name: ASC, age: DESC } }); + }); + + it('should return null for empty input', () => { + const result = schema.parse(''); + expect(result).toBeNull(); + }); + + it('should trim spaces and process input correctly', () => { + const result = schema.parse(' name , -age '); + expect(result).toEqual({ target: { name: ASC, age: DESC } }); + }); + + it('should handle a mix of nested and non-nested fields', () => { + const result = schema.parse('user.name,-age,user.email'); + expect(result).toEqual({ + user: { name: ASC, email: ASC }, + target: { age: DESC }, + }); + }); + + it('should return null if the input is undefined', () => { + const result = schema.parse(undefined); + expect(result).toBeNull(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts new file mode 100644 index 00000000..105fab4e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/sort.ts @@ -0,0 +1,37 @@ +import { z } from 'zod'; +import { ASC, DESC } from '../../../../constants'; +import { ZodInfer } from '../../types'; + +export function zodSortInputQuery() { + return z + .string() + .optional() + .transform((data) => { + if (!data) return null; + + return data + .split(',') + .map((i) => i.trim()) + .filter((i) => !!i) + .reduce((acum, field) => { + const fieldName = + field.charAt(0) === '-' ? field.substring(1) : field; + const sort = field.charAt(0) === '-' ? DESC : ASC; + if (fieldName.indexOf('.') > -1) { + const [relation, fieldRelation] = field.split('.'); + const relationName = + relation.charAt(0) === '-' ? relation.substring(1) : relation; + + acum[relationName] = acum[relationName] || {}; + acum[relationName][fieldRelation] = sort; + } else { + acum['target'] = acum['target'] || {}; + acum['target'][fieldName] = sort; + } + + return acum; + }, {} as Record>); + }); +} + +export type ZodSortInputQuery = ZodInfer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts new file mode 100644 index 00000000..1c0d88f1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts @@ -0,0 +1,160 @@ +import { zodFieldsQuery } from './fields'; +import { Users } from '../../../../mock-utils'; +import { RelationTree, ResultGetField } from '../../types'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; +const schema = zodFieldsQuery(userFields, userRelations); +describe('zodFieldsQuerySchema', () => { + it('should validate a target field correctly', () => { + const input = { target: ['id'] }; + const result = schema.safeParse(input); + + expect(result.success).toBe(true); + }); + + it('should validate a nested relation field correctly', () => { + const input = { roles: ['isDefault', 'key'] }; + const result = schema.safeParse(input); + + expect(result.success).toBe(true); + }); + + it('should fail validation if input is empty', () => { + const input = {}; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe( + 'Validation error: Select target or relation fields' + ); + } + }); + + it('should fail validation if an invalid target field is provided', () => { + const input = { target: 'invalid_field' }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain('Expected array'); + } + }); + + it('should fail validation if the relation object contains incorrect fields', () => { + const input = { posts: { invalidField: 'value' } }; + const result = schema.safeParse(input); + + expect(result.success).toBe(false); + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = { target: ['id'], extraField: 'not_allowed' }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Should be only target of relation' + ); + } + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = { target: ['id', 'id'] }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Field should be unique' + ); + } + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = { target1: ['id', 'id'] }; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Should be only target of relation' + ); + } + }); + + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = {}; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain( + 'Validation error: Select target or relation fields' + ); + } + }); + it('should ensure the schema is strict and does not allow extra fields', () => { + const input: [] = []; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain('Expected object'); + } + }); + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = null; + const result = schema.safeParse(input); + expect(result.success).toBe(true); + }); + it('should ensure the schema is strict and does not allow extra fields', () => { + const input = ''; + const result = schema.safeParse(input); + expect.assertions(2); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain('Expected object'); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts new file mode 100644 index 00000000..b301c636 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts @@ -0,0 +1,53 @@ +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; + +import { nonEmptyObject, uniqueArray } from '../zod-utils'; +import { ObjectLiteral } from '../../../../types'; +import { ResultGetField, RelationTree } from '../../types'; + +function getZodRules(fields: K) { + return z + .enum(fields) + .array() + .nonempty() + .refine(uniqueArray(), { + message: 'Field should be unique', + }) + .optional(); +} + +type ZodRule = ReturnType< + typeof getZodRules +>; + +export function zodFieldsQuery( + fields: ResultGetField['field'], + relationList: RelationTree +) { + const target = { + target: getZodRules(fields), + }; + + const relation = {} as { + [K in keyof RelationTree]: ZodRule[K]>; + }; + + for (const [key, value] of ObjectTyped.entries(relationList)) { + relation[key] = getZodRules(value); + } + + return z + .object({ + ...target, + ...relation, + }) + .strict('Should be only target of relation') + .refine(nonEmptyObject(), { + message: 'Validation error: Select target or relation fields', + }) + .nullable(); +} +export type ZodFieldsQuery = ReturnType< + typeof zodFieldsQuery +>; +export type FieldsQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts new file mode 100644 index 00000000..43972032 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts @@ -0,0 +1,381 @@ +import { zodFilterQuery } from './filter'; +import { Users } from '../../../../mock-utils'; +import { + RelationTree, + ResultGetField, + AllFieldWithType, + TypeField, + ArrayPropsForEntity, +} from '../../types'; +import { ZodError } from 'zod'; +import { ZodFilterInputQuery } from '../zod-input-query-schema/filter'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +const propsArray: ArrayPropsForEntity = { + target: { + testArrayNull: true, + testReal: true, + }, + addresses: { + arrayField: true, + }, + userGroup: {}, + manager: { + testArrayNull: true, + testReal: true, + }, + comments: {}, + notes: {}, + roles: {}, +}; + +const propsType: AllFieldWithType = { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + addresses: { + arrayField: TypeField.array, + country: TypeField.string, + state: TypeField.string, + city: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + manager: { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + }, + roles: { + isDefault: TypeField.boolean, + key: TypeField.string, + name: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + comments: { + kind: TypeField.string, + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + notes: { + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.string, + }, + userGroup: { + label: TypeField.string, + id: TypeField.number, + }, +}; + +const schema = zodFilterQuery( + userFields, + userRelations, + propsArray, + propsType +); + +describe('Check "filter" zod schema', () => { + describe('Valid schema', () => { + it('Valid schema - check1', () => { + const check1: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + ne: '12', + }, + }, + relation: null, + }; + const result = schema.parse(check1); + expect(result).toEqual(check1); + }); + + it('Valid schema - check2', () => { + const check2: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + }, + login: { + lt: 'sdfs', + }, + }, + relation: { + addresses: { + arrayField: { + some: ['sdfsdf', 'sdfsdf'], + }, + }, + }, + }; + const result = schema.parse(check2); + expect(result).toEqual(check2); + }); + + it('Valid schema - check3', () => { + const check3: ZodFilterInputQuery = { + target: null, + relation: null, + }; + const result = schema.parse(check3); + expect(result).toEqual(check3); + }); + + it('Valid schema - check4', () => { + const check4: ZodFilterInputQuery = { + target: null, + relation: { + comments: { + id: { + lte: '123', + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + }, + }; + const result = schema.parse(check4); + expect(result).toEqual(check4); + }); + + it('Valid schema - check5', () => { + const check5: ZodFilterInputQuery = { + target: null, + relation: { + comments: { + id: { + in: ['1'], + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + }, + }; + const result = schema.parse(check5); + expect(result).toEqual(check5); + }); + + it('Valid schema - check6', () => { + const check6: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + ne: '123', + }, + addresses: { + eq: 'null', + }, + }, + relation: null, + }; + const result = schema.parse(check6); + expect(result).toEqual(check6); + }); + + it('Valid schema - check7', () => { + const check7: ZodFilterInputQuery = { + target: { + isActive: { + eq: 'true', + }, + }, + relation: null, + }; + const result = schema.parse(check7); + expect(result).toEqual(check7); + }); + + it('Valid schema - check8', () => { + const check8: ZodFilterInputQuery = { + target: { + createdAt: { + eq: '2023-12-08T09:40:58.020Z', + }, + }, + relation: null, + }; + const result = schema.parse(check8); + expect(result).toEqual(check8); + }); + + it('Valid schema - check9', () => { + const check9: ZodFilterInputQuery = { + target: { + createdAt: { + eq: 'null', + }, + }, + relation: null, + }; + const result = schema.parse(check9); + expect(result.target!.createdAt!.eq).toEqual(null); + result.target!.createdAt!.eq = 'null'; + expect(result).toEqual(check9); + }); + + it('Valid schema - check10', () => { + const check: ZodFilterInputQuery = { + target: { + id: { + gte: '1213', + ne: '123', + }, + addresses: { + eq: null as any, + }, + }, + relation: null, + }; + const result = schema.parse(check); + expect(result).toEqual({ + ...check, + target: { + ...check.target, + addresses: { + eq: 'null', + }, + }, + }); + }); + }); + + describe('Invalid schema', () => { + it('Invalid schema - check1', () => { + const check1 = null; + expect(() => schema.parse(check1)).toThrow(ZodError); + }); + + it('Invalid schema - check2', () => { + const check2 = {}; + expect(() => schema.parse(check2)).toThrow(ZodError); + }); + + it('Invalid schema - check3', () => { + const check3 = ''; + expect(() => schema.parse(check3)).toThrow(ZodError); + }); + + it('Invalid schema - check4', () => { + const check4 = 1; + expect(() => schema.parse(check4)).toThrow(ZodError); + }); + + it('Invalid schema - check5', () => { + const check5: any[] = []; + expect(() => schema.parse(check5)).toThrow(ZodError); + }); + + it('Invalid schema - check6', () => { + const check6 = { + target: null, + }; + expect(() => schema.parse(check6)).toThrow(ZodError); + }); + + it('Invalid schema - check7', () => { + const check7 = { + target: null, + relation: { + commentsasda: { + id: { + lte: 'sdfsdf', + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + }, + }; + expect(() => schema.parse(check7)).toThrow(ZodError); + }); + + it('Invalid schema - check8', () => { + const check8 = { + target: null, + relation: { + comment: { + id: { + lte: 'sdfsdf', + }, + }, + manager: { + firstName: { + eq: 'sdfsdfsdf', + }, + }, + sdfsdf: {}, + }, + }; + expect(() => schema.parse(check8)).toThrow(ZodError); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts new file mode 100644 index 00000000..9653d77d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts @@ -0,0 +1,243 @@ +import { z, ZodOptional } from 'zod'; +import { + FilterOperand, + ObjectTyped, + FilterOperandOnlyInNin, + FilterOperandOnlySimple, +} from '@klerick/json-api-nestjs-shared'; + +import { + AllFieldWithType, + ArrayPropsForEntity, + RelationTree, + ResultGetField, + IsArray, + TypeField, + EntityProps, + TypeOfArray, + CastProps, + TypeCast, + PropsArray, +} from '../../types'; +import { ObjectLiteral } from '../../../../types'; +import { + stringLongerThan, + arrayItemStringLongerThan, + stringMustBe, + elementOfArrayMustBe, + oneOf, + guardIsKeyOfObject, + nonEmptyObject, +} from '../zod-utils'; + +const zodRuleForString = z.union([ + z.literal('null').transform(() => null), + z.string().refine(stringLongerThan(), { + message: 'String should be not empty', + }), +]); + +const zodRuleStringArray = zodRuleForString + .array() + .nonempty() + .refine(arrayItemStringLongerThan(0), { + message: 'Array should be not empty', + }); + +const zodNullRule = z.union([ + z.literal('null'), + z.literal(null).transform((r) => 'null' as const), +]); + +const zodRuleFilterRelationSchema = z.union([ + z + .object({ + [FilterOperand.eq]: zodNullRule, + }) + .strict(), + z + .object({ + [FilterOperand.ne]: zodNullRule, + }) + .strict(), +]); +const zodRuleForArrayField = z + .object({ [FilterOperand.some]: zodRuleStringArray }) + .strict(); + +function getZodRulesForField(type: TypeField = TypeField.string) { + const simpleShape = ObjectTyped.entries(FilterOperandOnlySimple).reduce( + (acum, [key, val]) => ({ + ...acum, + [val]: zodRuleForString + .refine(stringMustBe(type), { + message: `String should be as ${type}`, + }) + .optional(), + }), + {} as { + [K in FilterOperandOnlySimple]: ZodOptional; + } + ); + + const ninInShape = ObjectTyped.entries(FilterOperandOnlyInNin).reduce( + (acum, [key, val]) => ({ + ...acum, + [val]: zodRuleStringArray + .refine(elementOfArrayMustBe(type), { + message: `String should be as ${type}`, + }) + .optional(), + }), + {} as { + [K in FilterOperandOnlyInNin]: ZodOptional; + } + ); + return z + .object({ + ...simpleShape, + ...ninInShape, + }) + .strict() + .refine( + oneOf( + Object.values(FilterOperand).filter((i) => i !== FilterOperand.some) + ), + { + message: `Must have one of: "${Object.values(FilterOperand) + .filter((i) => i !== FilterOperand.some) + .join('","')}"`, + } + ); +} + +function getFilterPropsShapeForEntity( + fields: ResultGetField['field'], + propsArrayTarget: PropsArray, + propsType: AllFieldWithType +) { + return fields.reduce( + (acum, field) => ({ + ...acum, + [field]: (Reflect.get(propsArrayTarget, field) + ? zodRuleForArrayField + : getZodRulesForField(propsType[field as EntityProps]) + ).optional(), + }), + {} as FilterProps['field']> + ); +} + +function getZodRulesForRelationShape( + shape: FilterProps['field']> +) { + return z.object(shape).strict().optional().refine(nonEmptyObject()); +} + +type ZodRuleForString = typeof zodRuleForString; +type ZodRuleStringArray = typeof zodRuleStringArray; +type ZodRuleFilterRelationSchema = typeof zodRuleFilterRelationSchema; +type ZodRuleForArrayField = typeof zodRuleForArrayField; +type ZodRulesForField = ReturnType; +type ZodRulesForRelationShape = ReturnType< + typeof getZodRulesForRelationShape +>; + +type FilterProps< + E extends ObjectLiteral, + P extends readonly [string, ...string[]] +> = { + [Props in P[number]]: Props extends keyof E + ? IsArray extends true + ? ZodOptional + : ZodOptional + : never; +}; + +type RelationType = TypeCast< + TypeOfArray>, + ObjectLiteral +>; + +type RelationFilterProps = { + [R in keyof RelationTree]: ZodRulesForRelationShape>; +}; + +export function zodFilterQuery( + fields: ResultGetField['field'], + relationTree: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +) { + const { target: propsArrayTarget, ...otherRelationPropsArray } = propsArray; + + const fieldsFilterProps = getFilterPropsShapeForEntity( + fields, + propsArrayTarget, + propsType + ); + + const targetRelation = ObjectTyped.keys(relationTree).reduce( + (acum, item) => ({ + ...acum, + [item]: zodRuleFilterRelationSchema.optional(), + }), + {} as { + [K in ResultGetField['relations'][number]]: ZodOptional; + } + ); + + const relationFilterProps = ObjectTyped.keys(relationTree).reduce( + (acum, name) => { + type F = typeof name; + type RT = RelationType; + type RTF = ResultGetField['field']; + + guardIsKeyOfObject(otherRelationPropsArray, name); + const relationField = relationTree[name] as RTF; + const relationPropsArray = otherRelationPropsArray[ + name + ] as PropsArray; + const relationPropsType = propsType[name] as AllFieldWithType; + + const filterProps = getFilterPropsShapeForEntity( + relationField, + relationPropsArray, + relationPropsType + ); + + const zodFilter = getZodRulesForRelationShape(filterProps); + + return { + ...acum, + [name]: zodFilter.optional(), + }; + }, + {} as RelationFilterProps + ); + + const targetShapeFilter = { + target: z + .object({ + ...fieldsFilterProps, + ...targetRelation, + }) + .strict() + .optional() + .refine(nonEmptyObject()) + .nullable(), + relation: z + .object(relationFilterProps) + .strict() + .optional() + .refine(nonEmptyObject()) + .nullable(), + }; + + return z.object(targetShapeFilter).strict().refine(nonEmptyObject()); +} + +export type ZodFilterQuery = ReturnType< + typeof zodFilterQuery +>; +export type FilterQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts new file mode 100644 index 00000000..6283395c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts @@ -0,0 +1,53 @@ +import { zodIncludeQuery } from './include'; +import { ResultGetField } from '../../types'; +import { Users } from '../../../../mock-utils'; + +const relationList: ResultGetField['relations'] = [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', +]; +const schema = zodIncludeQuery(relationList); + +describe('zodIncludeQuery', () => { + it('should validate an array of relations successfully', () => { + const result = schema.parse(['comments', 'addresses']); + + expect(result).toEqual(['comments', 'addresses']); + }); + + it('should return null if input is null', () => { + const result = schema.parse(null); + + expect(result).toBeNull(); + }); + + it('should throw an error if the array is empty', () => { + expect(() => schema.parse([])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining( + 'Array must contain at least 1 element' + ), + }) + ); + }); + + it('should throw an error if the array has duplicate entries', () => { + expect(() => schema.parse(['addresses', 'addresses'])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('Include should have unique relation'), + }) + ); + }); + + it('should throw an error if the input contains invalid relations', () => { + expect(() => schema.parse(['invalid_relation'])).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('Invalid enum value. Expected'), + }) + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts new file mode 100644 index 00000000..cf8e87a0 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.ts @@ -0,0 +1,22 @@ +import { z } from 'zod'; +import { ObjectLiteral } from '../../../../types'; +import { ResultGetField } from '../../types'; +import { uniqueArray } from '../zod-utils'; + +export function zodIncludeQuery( + relationList: ResultGetField['relations'] +) { + return z + .enum(relationList) + .array() + .nonempty() + .refine(uniqueArray(), { + message: 'Include should have unique relation', + }) + .nullable(); +} + +export type ZodIncludeQuery = ReturnType< + typeof zodIncludeQuery +>; +export type IncludeQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts new file mode 100644 index 00000000..71a5eb51 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts @@ -0,0 +1,238 @@ +import { FilterOperand, QueryField } from '@klerick/json-api-nestjs-shared'; +import { zodQuery } from './index'; +import { + AllFieldWithType, + ArrayPropsForEntity, + RelationTree, + ResultGetField, + TypeField, +} from '../../types'; +import { Users } from '../../../../mock-utils'; +import { InputQuery } from '../zod-input-query-schema'; +import { ASC } from '../../../../constants'; + +const userFields: ResultGetField = { + field: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + relations: [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ], +}; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +const propsArray: ArrayPropsForEntity = { + target: { + testArrayNull: true, + testReal: true, + }, + addresses: { + arrayField: true, + }, + userGroup: {}, + manager: { + testArrayNull: true, + testReal: true, + }, + comments: {}, + notes: {}, + roles: {}, +}; + +const propsType: AllFieldWithType = { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + addresses: { + arrayField: TypeField.array, + country: TypeField.string, + state: TypeField.string, + city: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + manager: { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + }, + roles: { + isDefault: TypeField.boolean, + key: TypeField.string, + name: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + comments: { + kind: TypeField.string, + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + notes: { + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.string, + }, + userGroup: { + label: TypeField.string, + id: TypeField.number, + }, +}; + +const schemaQuery = zodQuery( + userFields, + userRelations, + propsArray, + propsType +); + +describe('schemaQuery.parse', () => { + it('should successfully parse valid input', () => { + const validInput: InputQuery = { + [QueryField.fields]: { + target: [ + 'id', + 'login', + 'firstName', + 'lastName', + 'createdAt', + 'updatedAt', + ], + }, + [QueryField.filter]: { relation: null, target: null }, + [QueryField.include]: ['roles'], + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schemaQuery.parse(validInput)).not.toThrow(); + const result = schemaQuery.parse(validInput); + expect(result).toEqual(validInput); + }); + + it('should throw an error for invalid field values', () => { + const invalidInput = { + field: ['invalidField'], // Field not defined in userFields + relations: { + comments: ['text', 'id'], + }, + }; + + expect(() => schemaQuery.parse(invalidInput)).toThrow(); + }); + + it('should throw an error if a required relation field is missing', () => { + const invalidInput = { + field: ['updatedAt', 'createdAt', 'login'], + relations: {}, // Missing required relations + }; + + expect(() => schemaQuery.parse(invalidInput)).toThrowError(/invalid_type/i); + }); + + it('should handle nested relations', () => { + const validInput: InputQuery = { + [QueryField.fields]: { + target: [ + 'id', + 'login', + 'firstName', + 'lastName', + 'createdAt', + 'updatedAt', + ], + }, + [QueryField.filter]: { + relation: null, + target: { + id: { + [FilterOperand.in]: ['1', '2', '3'], + }, + }, + }, + [QueryField.include]: ['roles'], + [QueryField.sort]: { target: { id: ASC } }, + [QueryField.page]: { number: 1, size: 10 }, + }; + + expect(() => schemaQuery.parse(validInput)).not.toThrow(); + const result = schemaQuery.parse(validInput); + expect(result).toEqual(validInput); + }); + + it('should throw an error for invalid nested relations', () => { + const invalidInput = { + field: ['id', 'login'], + relations: { + manager: { + field: ['id', 'nonExistentField'], + relations: { + roles: ['id', 'name'], + }, + }, + }, + }; + + expect(() => schemaQuery.parse(invalidInput)).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts new file mode 100644 index 00000000..4025636f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts @@ -0,0 +1,99 @@ +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { z, ZodObject } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { + AllFieldWithType, + ArrayPropsForEntity, + RelationTree, + ResultGetField, +} from '../../types'; + +import { zodFieldsQuery, ZodFieldsQuery } from './fields'; +import { zodFilterQuery, ZodFilterQuery } from './filter'; +import { zodSortQuery, ZodSortQuery } from './sort'; +import { zodIncludeQuery, ZodIncludeQuery } from './include'; +import { zodPageInputQuery, ZodPageInputQuery } from '../zod-share'; + +type Shape = { + [QueryField.fields]: ZodFieldsQuery; + [QueryField.filter]: ZodFilterQuery; + [QueryField.include]: ZodIncludeQuery; + [QueryField.sort]: ZodSortQuery; + [QueryField.page]: ZodPageInputQuery; +}; + +function getShape( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +): Shape { + return { + [QueryField.fields]: zodFieldsQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.filter]: zodFilterQuery( + entityFieldsStructure.field, + entityRelationStructure, + propsArray, + propsType + ), + [QueryField.include]: zodIncludeQuery(entityFieldsStructure.relations), + [QueryField.sort]: zodSortQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.page]: zodPageInputQuery(), + }; +} + +function getZodResultSchema( + shape: Shape +): ZodObject, 'strict'> { + return z.object(shape).strict(); +} + +export function zodQuery( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +): ZodResultSchema { + const shape = getShape( + entityFieldsStructure, + entityRelationStructure, + propsArray, + propsType + ); + return getZodResultSchema(shape); +} + +export type ZodResultSchema = ReturnType< + typeof getZodResultSchema +>; +export type ZodQuery = ReturnType>; +export type Query = z.infer>; + +function zodQueryOne( + entityFieldsStructure: ResultGetField, + entityRelationStructure: RelationTree, + propsArray: ArrayPropsForEntity, + propsType: AllFieldWithType +) { + return z + .object({ + [QueryField.fields]: zodFieldsQuery( + entityFieldsStructure.field, + entityRelationStructure + ), + [QueryField.include]: zodIncludeQuery(entityFieldsStructure.relations), + }) + .strict(); +} + +export type ZodQueryOne = ReturnType< + typeof zodQueryOne +>; +export type QueryOne = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts new file mode 100644 index 00000000..519c931d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts @@ -0,0 +1,126 @@ +import { zodSortQuery } from './sort'; + +import { RelationTree, ResultGetField } from '../../types'; +import { Users } from '../../../../mock-utils'; +import { ASC, DESC } from '../../../../constants'; + +const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; +const schema = zodSortQuery(userFields, userRelations); +describe('zodSortQuery', () => { + it('should create a Zod schema with target and relations', () => { + const parsedData = schema.parse({ + target: { id: ASC }, + addresses: { country: DESC }, + manager: { lastName: ASC }, + roles: { name: DESC }, + comments: { kind: DESC }, + notes: { text: ASC }, + userGroup: { label: ASC }, + }); + + expect(parsedData).toEqual({ + target: { id: ASC }, + addresses: { country: DESC }, + manager: { lastName: ASC }, + roles: { name: DESC }, + comments: { kind: DESC }, + notes: { text: ASC }, + userGroup: { label: ASC }, + }); + }); + + it('should throw an error for an invalid field in target', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse({ + target: 'invalid_value', + }); + }).toThrowError(); + }); + + it('should throw an error for invalid fields in relations', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse({ + addresses: 'invalid_value', + }); + }).toThrowError(); + }); + + it('should allow partial relations and target', () => { + const schema = zodSortQuery(userFields, userRelations); + + const parsedData = schema.parse({ + target: { id: ASC }, + addresses: { country: DESC }, + }); + + expect(parsedData).toEqual({ + target: { id: ASC }, + addresses: { country: DESC }, + }); + }); + + it('should fail if an empty object is not allowed', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse({}); + }).toThrowError(); + }); + it('null should be valid', () => { + expect(schema.parse(null)).toBe(null); + }); + + it('should fail if the input is not a valid object', () => { + const schema = zodSortQuery(userFields, userRelations); + + expect(() => { + schema.parse([]); + }).toThrowError(); + expect(() => { + schema.parse('invalid'); + }).toThrowError(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts new file mode 100644 index 00000000..f129fdb0 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts @@ -0,0 +1,60 @@ +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { z } from 'zod'; + +import { RelationTree, ResultGetField } from '../../types'; +import { ObjectLiteral } from '../../../../types'; +import { SORT_TYPE } from '../../../../constants'; +import { nonEmptyObject } from '../zod-utils'; + +function getZodSortRule() { + return z.enum(SORT_TYPE).optional(); +} + +function getZodFieldRule(fields: F) { + const targetShape = fields.reduce( + (acum, item) => ({ + ...acum, + [item]: getZodSortRule(), + }), + {} as { [K in F[number]]: ZodSortRule } + ); + + return z.object(targetShape).strict().refine(nonEmptyObject()).optional(); +} + +type ZodSortRule = ReturnType; +type ZodFieldRule = ReturnType< + typeof getZodFieldRule +>; + +export function zodSortQuery( + fields: ResultGetField['field'], + relationList: RelationTree +) { + const zodRelationShape = {} as { + [K in keyof RelationTree]: ZodFieldRule[K]>; + }; + const zodTargetShape: { target: ZodFieldRule['field']> } = { + target: getZodFieldRule(fields), + }; + + for (const [key, val] of ObjectTyped.entries(relationList)) { + if (key === 'target') continue; + zodRelationShape[key] = getZodFieldRule(val); + } + + return z + .object({ + ...zodTargetShape, + ...zodRelationShape, + }) + .strict() + .partial() + .refine(nonEmptyObject()) + .nullable(); +} + +export type ZodSortQuery = ReturnType< + typeof zodSortQuery +>; +export type SortQuery = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts new file mode 100644 index 00000000..b9b7d3a9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts @@ -0,0 +1,200 @@ +import { z, ZodError } from 'zod'; +import { zodAttributes, ZodAttributes, Attributes } from './attributes'; +import { Addresses, Users } from '../../../../mock-utils'; +import { FieldWithType, PropsForField, TypeField } from '../../types'; + +const fieldTypeUsers: FieldWithType = { + id: TypeField.number, + isActive: TypeField.boolean, + firstName: TypeField.string, + createdAt: TypeField.date, + lastName: TypeField.string, + login: TypeField.string, + testDate: TypeField.date, + updatedAt: TypeField.date, + testReal: TypeField.array, + testArrayNull: TypeField.array, +}; +const propsDb: PropsForField = { + id: { type: Number, isArray: false, isNullable: false }, + login: { type: 'varchar', isArray: false, isNullable: false }, + firstName: { type: 'varchar', isArray: false, isNullable: true }, + testReal: { type: 'real', isArray: true, isNullable: false }, + testArrayNull: { type: 'real', isArray: true, isNullable: true }, + lastName: { type: 'varchar', isArray: false, isNullable: true }, + isActive: { type: 'boolean', isArray: false, isNullable: true }, + createdAt: { type: 'timestamp', isArray: false, isNullable: true }, + testDate: { type: 'timestamp', isArray: false, isNullable: true }, + updatedAt: { type: 'timestamp', isArray: false, isNullable: true }, +}; +const fieldTypeAddresses: FieldWithType = { + id: TypeField.number, + arrayField: TypeField.array, + state: TypeField.string, + city: TypeField.string, + createdAt: TypeField.date, + updatedAt: TypeField.date, + country: TypeField.string, +}; + +describe('attributes', () => { + type SchemaTypeUsers = Attributes; + type SchemaTypeAddresses = Attributes; + + describe('Attributes for post', () => { + let schemaUsers: ZodAttributes; + let schemaAddresses: ZodAttributes; + beforeEach(() => { + schemaUsers = zodAttributes(fieldTypeUsers, propsDb, 'id', false); + schemaAddresses = zodAttributes( + fieldTypeAddresses, + {} as PropsForField, + 'id', + false + ); + }); + + it('should be ok', () => { + const date = new Date(); + const check: SchemaTypeUsers = { + login: 'login', + isActive: true, + lastName: 'sdsdf', + testReal: [123.123, 123.123], + testArrayNull: [], + testDate: date.toISOString() as any, + }; + + const check2: SchemaTypeAddresses = { + arrayField: ['test', 'test'], + state: 'state', + country: 'country', + city: 'city', + createdAt: date.toISOString() as any, + updatedAt: date.toISOString() as any, + }; + + const check3: SchemaTypeUsers = { + login: 'login', + isActive: true, + lastName: 'sdsdf', + testReal: [123.123, 123.123], + testArrayNull: null as any, + testDate: date.toISOString() as any, + }; + + expect(schemaUsers.parse(check)).toEqual({ + ...check, + testDate: date, + }); + expect(schemaAddresses.parse(check2)).toEqual({ + ...check2, + createdAt: date, + updatedAt: date, + }); + + expect(schemaUsers.parse(check3)).toEqual({ + ...check3, + testDate: date, + }); + }); + + it('should be not ok', () => { + const check = { + id: '1', + isActive: 'true', + lastName: 1, + }; + const check2 = { + arrayField: 'test', + }; + expect.assertions(2); + try { + expect(schemaUsers.parse(check)).toEqual(check); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + try { + schemaAddresses.parse(check2); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); + }); + + describe('Attributes for patch', () => { + let schemaUsers: ZodAttributes; + let schemaAddresses: ZodAttributes; + beforeEach(() => { + schemaUsers = zodAttributes( + fieldTypeUsers, + propsDb, + 'id', + true + ); + schemaAddresses = zodAttributes( + fieldTypeAddresses, + {} as PropsForField, + 'id', + true + ); + }); + + it('should be ok', () => { + const date = new Date(); + const check: SchemaTypeUsers = { + login: 'login', + isActive: true, + testDate: date.toISOString() as any, + }; + + const check2: SchemaTypeAddresses = { + arrayField: ['test', 'test'], + state: 'state', + createdAt: date.toISOString() as any, + updatedAt: date.toISOString() as any, + }; + + const check3: SchemaTypeUsers = { + testReal: [123.123, 123.123], + testArrayNull: null as any, + testDate: date.toISOString() as any, + }; + + expect(schemaUsers.parse(check)).toEqual({ + ...check, + testDate: date, + }); + expect(schemaAddresses.parse(check2)).toEqual({ + ...check2, + createdAt: date, + updatedAt: date, + }); + + expect(schemaUsers.parse(check3)).toEqual({ + ...check3, + testDate: date, + }); + }); + + it('should be not ok', () => { + const check = { + id: '1', + }; + const check2 = { + arrayField: 'test', + }; + expect.assertions(2); + try { + expect(schemaUsers.parse(check)).toEqual(check); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + try { + schemaAddresses.parse(check2); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts new file mode 100644 index 00000000..d00503f7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts @@ -0,0 +1,202 @@ +import { + EntityProps, + ObjectTyped, + TypeOfArray, +} from '@klerick/json-api-nestjs-shared'; +import { z, ZodArray, ZodNullable } from 'zod'; + +import { ObjectLiteral } from '../../../../types'; +import { + FieldWithType, + PropsFieldItem, + PropsForField, + TypeField, +} from '../../types'; +import { nonEmptyObject } from '../zod-utils'; + +const literalSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]); +type Literal = z.infer; +type Json = Literal | { [key: string]: Json } | Json[]; + +function getZodRulesForNumber(isNullable: boolean) { + const schema = z.preprocess((x) => Number(x), z.number()); + if (isNullable) schema.nullable().optional(); + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForString(isNullable: boolean) { + const schema = z.string(); + if (isNullable) schema.nullable().optional(); + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForDate(isNullable: boolean) { + const schema = z.coerce.date(); + if (isNullable) schema.nullable().optional(); + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForBoolean(isNullable: boolean) { + const schema = z.boolean(); + if (isNullable) schema.nullable(); + return isNullable ? schema.optional() : schema; +} + +function getZodSchemaForJson(isNullable: boolean) { + const tmpSchema = isNullable ? literalSchema.nullable() : literalSchema; + + const schema: z.ZodType = z.lazy(() => + z.union([tmpSchema, z.array(tmpSchema), z.record(tmpSchema)]) + ); + + return isNullable ? schema.optional() : schema; +} + +function getZodRulesForArray( + propsField: PropsFieldItem +): + | ZodArray, 'many'> + | ZodNullable, 'many'>> { + const type = propsField.type as T; + let schema: ZodRulesForArray; + + if (!propsField) { + schema = getZodRulesForString(false) as ZodRulesForArray; + } else { + switch (type) { + case 'number': + case 'real': + case 'integer': + case 'bigint': + case 'double': + case 'numeric': + case Number: + schema = getZodRulesForNumber(false) as ZodRulesForArray; + break; + case 'date': + case Date: + schema = getZodRulesForDate(false) as ZodRulesForArray; + break; + case 'boolean': + case Boolean: + schema = getZodRulesForBoolean(false) as ZodRulesForArray; + break; + default: + schema = getZodRulesForString(false) as ZodRulesForArray; + } + } + + if (propsField.isNullable) { + return schema.array().nullable() as ZodNullable< + ZodArray, 'many'> + >; + } + return schema.array() as ZodArray, 'many'>; +} + +type ZodRulesForArray = T extends number + ? ReturnType + : T extends Date + ? ReturnType + : T extends boolean + ? ReturnType + : ReturnType; + +type ZodRulesForType = ReturnType< + T extends TypeField.array + ? typeof getZodRulesForArray + : T extends TypeField.date + ? typeof getZodRulesForDate + : T extends TypeField.boolean + ? typeof getZodRulesForBoolean + : T extends TypeField.number + ? typeof getZodRulesForNumber + : T extends TypeField.object + ? typeof getZodSchemaForJson + : typeof getZodRulesForString +>; + +function buildSchema( + fieldType: T, + propsField: P +): ZodRulesForType { + let schema: ZodRulesForType; + switch (fieldType) { + case TypeField.array: + schema = getZodRulesForArray(propsField) as ZodRulesForType; + break; + case TypeField.date: + schema = getZodRulesForDate(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + case TypeField.boolean: + schema = getZodRulesForBoolean(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + case TypeField.number: + schema = getZodRulesForNumber(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + case TypeField.object: + schema = getZodSchemaForJson(propsField.isNullable) as ZodRulesForType< + T, + I + >; + break; + default: + schema = getZodRulesForString(propsField.isNullable) as ZodRulesForType< + T, + I + >; + } + + return schema; +} + +export function zodAttributes< + E extends ObjectLiteral, + S extends true | false = false +>( + fieldWithType: FieldWithType, + propsDb: PropsForField, + primaryColumn: EntityProps, + isPatch: S +) { + const objectShape = {} as { + [K in keyof Omit, keyof EntityProps>]: ZodRulesForType< + FieldWithType[K], + TypeOfArray + >; + }; + + for (const [nameList, type] of ObjectTyped.entries(fieldWithType)) { + if (nameList === primaryColumn) continue; + const name = nameList as keyof Omit, keyof EntityProps>; + const propsField = propsDb[name]; + objectShape[name] = buildSchema< + typeof type, + typeof propsField, + TypeOfArray + >(type, propsField || {}); + } + const zodSchema = z.object(objectShape).strict(); + if (isPatch) { + return zodSchema.partial().refine(nonEmptyObject()); + } + return zodSchema.refine(nonEmptyObject()); +} + +export type ZodAttributes< + E extends ObjectLiteral, + K extends true | false = false +> = ReturnType>; +export type Attributes< + E extends ObjectLiteral, + K extends true | false = false +> = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts new file mode 100644 index 00000000..d5ba401b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.spec.ts @@ -0,0 +1,37 @@ +import { ZodError } from 'zod'; + +import { ZodId, zodId } from './id'; +import { TypeField } from '../../types'; + +describe('zodIdSchema', () => { + let numberStringSchema: ZodId; + let stringSchema: ZodId; + beforeAll(() => { + numberStringSchema = zodId(TypeField.number); + stringSchema = zodId(TypeField.string); + }); + + it('Should be correct', () => { + const check1 = '1'; + const check2 = '12'; + const check3 = '123'; + const check4 = '-123'; + + const check5 = 'sfdsf'; + const checkArray = [check1, check2, check3, check4]; + for (const item of checkArray) { + expect(numberStringSchema.parse(item)).toBe(item); + } + expect(stringSchema.parse(check5)).toBe(check5); + }); + + it('Should be not ok', () => { + expect.assertions(1); + + try { + numberStringSchema.parse('sdfdfsfsf'); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts new file mode 100644 index 00000000..030ed201 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/id.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; +import { TypeField, TypeForId } from '../../types'; + +const reg = new RegExp('^-?\\d+$'); + +export function zodId(typeId: TypeForId) { + let idSchema = z.string(); + if (typeId === TypeField.number) { + idSchema = idSchema.regex(reg); + } + + return idSchema; +} + +export type ZodId = ReturnType; +export type Id = z.infer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts new file mode 100644 index 00000000..4b1fc19a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/index.ts @@ -0,0 +1,6 @@ +export * from './page'; +export * from './id'; +export * from './type'; +export * from './attributes'; +export * from './rel-data'; +export * from './relationships'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts new file mode 100644 index 00000000..6123a0c1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.spec.ts @@ -0,0 +1,42 @@ +import { zodPageInputQuery } from './page'; +import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../../constants'; + +describe('zodPageInputQuerySchema', () => { + const schema = zodPageInputQuery(); + + it('should parse valid size and number as number string', () => { + const result = schema.parse({ size: '5', number: '2' }); + expect(result).toEqual({ size: 5, number: 2 }); + }); + + it('should parse valid size and number', () => { + const result = schema.parse({ size: 5, number: 2 }); + expect(result).toEqual({ size: 5, number: 2 }); + }); + + it('should use the default size and number if not provided', () => { + const result = schema.parse({}); + expect(result).toEqual({ + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }); + }); + + it('should fail if size is less than 1', () => { + expect(() => schema.parse({ size: '0', number: '2' })).toThrow(); + }); + + it('should fail if number is less than 1', () => { + expect(() => schema.parse({ size: '5', number: '0' })).toThrow(); + }); + + it('should error size and number is number of float', () => { + expect(() => schema.parse({ size: '5.7', number: '2.9' })).toThrow(); + }); + + it('should error if has additional properties', () => { + expect(() => + schema.parse({ size: '5', number: '2', extra: 'ignored' }) + ).toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts new file mode 100644 index 00000000..2682313b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/page.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; +import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../../constants'; +import { ObjectLiteral } from '../../../../types'; + +export function zodPageInputQuery() { + return z + .object({ + size: z + .preprocess((x) => Number(x), z.number().int().min(1)) + .default(DEFAULT_PAGE_SIZE), + number: z + .preprocess((x) => Number(x), z.number().int().min(1)) + .default(DEFAULT_QUERY_PAGE), + }) + .strict() + .default({ + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }); +} + +export type ZodPageInputQuery = ReturnType; +export type PageInputQuery = z.infer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts new file mode 100644 index 00000000..fe3e3122 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.spec.ts @@ -0,0 +1,37 @@ +import { zodRelData, ZodRelData } from './rel-data'; +import { TypeField } from '../../types'; +import { ZodError } from 'zod'; + +describe('zodDataSchema', () => { + let zodData: ZodRelData; + beforeAll(() => { + zodData = zodRelData('users', TypeField.string); + }); + + it('Should be ok', () => { + const check = { + type: 'users', + id: 'id', + }; + expect(zodData.parse(check)).toEqual(check); + }); + + it('Should be not ok', () => { + const check = {}; + const check1 = { + test: '1', + }; + const check3: any[] = []; + const check4 = 'adfsdf'; + const check5 = true; + const checkArray = [check, check1, check3, check4, check5]; + expect.assertions(checkArray.length); + for (const item of checkArray) { + try { + zodData.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts new file mode 100644 index 00000000..619b9fc1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/rel-data.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +import { TypeForId } from '../../types'; +import { nonEmptyObject } from '../zod-utils'; + +import { zodType, zodId } from './'; + +export function zodRelData(typeName: T, typeId: TypeForId) { + return z + .object({ + id: zodId(typeId), + type: zodType(typeName), + }) + .strict() + .refine(nonEmptyObject); +} + +export type ZodRelData = ReturnType>; +export type RelData = z.infer>; + +export function zodData() { + return z + .object({ + type: z.coerce.string(), + id: z.coerce.string(), + }) + .strict() + .refine(nonEmptyObject); +} + +export type ZodData = ReturnType; +export type Data = z.infer; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts new file mode 100644 index 00000000..642a9397 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts @@ -0,0 +1,270 @@ +import { z, ZodError } from 'zod'; + +import { zodRelationships, ZodRelationships } from './relationships'; +import { Users } from '../../../../mock-utils'; +import { + RelationPropsArray, + RelationPropsTypeName, + RelationPrimaryColumnType, + TypeField, +} from '../../types'; + +describe('zodRelationships', () => { + const relationArrayProps: RelationPropsArray = { + roles: true, + userGroup: false, + notes: true, + addresses: false, + comments: true, + manager: false, + }; + const relationPopsName: RelationPropsTypeName = { + roles: 'Roles', + userGroup: 'UserGroups', + notes: 'Notes', + addresses: 'Addresses', + comments: 'Comments', + manager: 'Users', + }; + + const primaryColumnType: RelationPrimaryColumnType = { + roles: TypeField.number, + userGroup: TypeField.number, + notes: TypeField.string, + addresses: TypeField.number, + comments: TypeField.number, + manager: TypeField.number, + }; + + let relationshipsSchema: ZodRelationships; + + describe('POST', () => { + beforeAll(() => { + relationshipsSchema = zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + false + ); + }); + + it('Should be ok', () => { + const check = { + comments: { + data: [ + { + type: 'comments', + id: '1', + }, + ], + }, + userGroup: { + data: { + type: 'user-groups', + id: '1', + }, + }, + manager: { + data: { + type: 'users', + id: '1', + }, + }, + notes: { + data: [ + { + type: 'notes', + id: 'id', + }, + ], + }, + }; + expect(relationshipsSchema.parse(check)).toEqual(check); + }); + + it('should be not ok', () => { + const check1 = {}; + const check2 = ''; + const check3: any[] = []; + const check4 = true; + const check5 = { + sddsf: {}, + }; + const check6 = { + comments: [], + }; + const check7 = { + comments: {}, + }; + const check8 = { + comments: '', + }; + const check9 = { + comments: true, + }; + const check10 = { + comments: [ + { + sdsf: 'sdfsdf', + }, + ], + }; + const check11 = { + comments: [{}], + }; + const check12 = { + manager: {}, + }; + const check13 = { + manager: { + sdfs: 'sdsdf', + }, + }; + const check14 = { + manager: { + id: 'sdsdf', + type: 'users', + }, + }; + const check15 = { + manager: null, + }; + const check16 = { + manager: [], + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + check10, + check11, + check12, + check13, + check14, + check15, + check16, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + relationshipsSchema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); + + describe('PATCH', () => { + beforeAll(() => { + relationshipsSchema = zodRelationships( + relationArrayProps, + relationPopsName, + primaryColumnType, + true + ); + }); + + it('Should be ok', () => { + const check = { + comments: { + data: [], + }, + userGroup: { + data: null, + }, + manager: { + data: { + type: 'users', + id: '1', + }, + }, + notes: { + data: [ + { + type: 'notes', + id: 'id', + }, + ], + }, + }; + expect(relationshipsSchema.parse(check)).toEqual(check); + }); + + it('should be not ok', () => { + const check1 = {}; + const check2 = ''; + const check3: any[] = []; + const check4 = true; + const check5 = { + sddsf: {}, + }; + const check6 = { + comments: [], + }; + const check7 = { + comments: {}, + }; + const check8 = { + comments: '', + }; + const check9 = { + comments: true, + }; + const check10 = { + comments: [ + { + sdsf: 'sdfsdf', + }, + ], + }; + const check11 = { + comments: [{}], + }; + const check12 = { + manager: {}, + }; + const check13 = { + manager: { + sdfs: 'sdsdf', + }, + }; + const check14 = { + manager: { + id: 'sdsdf', + type: 'users', + }, + }; + const arrayCheck = [ + check1, + check2, + check3, + check4, + check5, + check6, + check7, + check8, + check9, + check10, + check11, + check12, + check13, + check14, + ]; + expect.assertions(arrayCheck.length); + for (const item of arrayCheck) { + try { + relationshipsSchema.parse(item); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts new file mode 100644 index 00000000..425253b8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts @@ -0,0 +1,157 @@ +import { z } from 'zod'; +import { + camelToKebab, + KebabCase, + ObjectTyped, +} from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; + +import { + RelationPropsArray, + RelationPropsTypeName, + RelationPrimaryColumnType, + TypeForId, +} from '../../types'; +import { zodRelData } from './rel-data'; +import { nonEmptyObject } from '../zod-utils'; + +function getZodRuleForData< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + if (isPatch) { + return zodRelData(typeName, primaryType).nullable(); + } + return zodRelData(typeName, primaryType); +} + +function getZodRuleForArrayData< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + const dataArraySchema = getZodRuleForData( + typeName, + primaryType, + false + ).array(); + if (isPatch) { + return dataArraySchema; + } + return dataArraySchema.nonempty(); +} + +function getZodDataShape< + K extends string, + P extends TypeForId, + I extends true, + T extends true | false = false +>( + typeName: K, + primaryType: P, + isArray: I, + isPatch: T +): ReturnType>; +function getZodDataShape< + K extends string, + P extends TypeForId, + I extends false, + T extends true | false = false +>( + typeName: K, + primaryType: P, + isArray: I, + isPatch: T +): ReturnType>; +function getZodDataShape< + K extends string, + P extends TypeForId, + I extends boolean, + T extends true | false = false +>( + typeName: K, + primaryType: P, + isArray: I, + isPatch: T +): ReturnType< + typeof getZodRuleForArrayData | typeof getZodRuleForData +> { + return isArray + ? getZodRuleForArrayData(typeName, primaryType, isPatch) + : getZodRuleForData(typeName, primaryType, isPatch); +} + +function getZodResultData< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + return z + .object({ + data: getZodDataShape(typeName, primaryType, false, isPatch), + }) + .optional(); +} + +function getZodResultDataArray< + K extends string, + P extends TypeForId, + T extends true | false = false +>(typeName: K, primaryType: P, isPatch: T) { + return z + .object({ + data: getZodDataShape(typeName, primaryType, true, isPatch), + }) + .optional(); +} + +type ZodResultData< + K extends string, + P extends TypeForId, + I extends boolean, + T extends true | false = false +> = I extends true + ? ReturnType> + : ReturnType>; + +export function zodRelationships< + E extends ObjectLiteral, + S extends true | false = false +>( + relationArrayProps: RelationPropsArray, + relationPopsName: RelationPropsTypeName, + primaryColumnType: RelationPrimaryColumnType, + isPatch: S +) { + const shape = {} as { + [K in keyof RelationPropsArray]: ZodResultData< + KebabCase[K]>, + RelationPrimaryColumnType[K], + RelationPropsArray[K], + S + >; + }; + + for (const [props, value] of ObjectTyped.entries(relationArrayProps)) { + const typeName = camelToKebab(relationPopsName[props]); + const primaryType = primaryColumnType[props]; + shape[props] = ( + value === true + ? getZodResultDataArray(typeName, primaryType, isPatch) + : getZodResultData(typeName, primaryType, isPatch) + ) as ZodResultData; + } + + return z.object(shape).strict().refine(nonEmptyObject()); +} + +export type ZodRelationships< + T extends ObjectLiteral, + K extends true | false = false +> = ReturnType>; +export type Relationships< + T extends ObjectLiteral, + K extends true | false = false +> = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts new file mode 100644 index 00000000..a5d5bd3b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.spec.ts @@ -0,0 +1,21 @@ +import { zodType, ZodType } from './type'; +import { ZodError } from 'zod'; + +describe('type', () => { + const literal = 'users'; + let userTypeSchema: ZodType; + beforeAll(() => { + userTypeSchema = zodType(literal); + }); + it('should be ok', () => { + expect(userTypeSchema.parse(literal)).toEqual(literal); + }); + it('should be ok', () => { + expect.assertions(1); + try { + userTypeSchema.parse('test'); + } catch (e) { + expect(e).toBeInstanceOf(ZodError); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts new file mode 100644 index 00000000..297952d1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/type.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export function zodType(type: T) { + return z.literal(type); +} + +export type ZodType = ReturnType>; +export type Type = z.infer>; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts new file mode 100644 index 00000000..decec6dc --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts @@ -0,0 +1,359 @@ +import { + arrayItemStringLongerThan, + elementOfArrayMustBe, + getValidationErrorForStrict, + nonEmptyObject, + oneOf, + stringLongerThan, + stringMustBe, + guardIsKeyOfObject, +} from './zod-utils'; +import { TypeField } from '../types'; + +describe('zod-utils', () => { + describe('guardIsKeyOfObject', () => { + /** + * Function Description: + * The `guardIsKeyOfObject` function acts as a type guard that ensures the given `key` + * is a valid key of the provided object `R`. If the key exists in the object, the type check passes. + * Otherwise, it throws an error. + */ + it('should not throw an error if the key exists in the object', () => { + const obj = { a: 1, b: 2, c: 3 }; + expect(() => guardIsKeyOfObject(obj, 'a')).not.toThrow(); + }); + + it('should throw an error if the key does not exist in the object', () => { + const obj = { a: 1, b: 2, c: 3 }; + expect(() => guardIsKeyOfObject(obj, 'z')).toThrow('Type guard error'); + }); + + it('should throw an error when the object is null', () => { + const obj = null; + expect(() => guardIsKeyOfObject(obj, 'a')).toThrow('Type guard error'); + }); + + it('should throw an error when the object is undefined', () => { + const obj = undefined; + expect(() => guardIsKeyOfObject(obj as any, 'a')).toThrow( + 'Type guard error' + ); + }); + + it('should throw an error for non-object types', () => { + const nonObject = 42; // A number instead of an object + expect(() => guardIsKeyOfObject(nonObject as any, 'a')).toThrow( + 'Type guard error' + ); + }); + + it('should work with symbol keys in the object', () => { + const symbolKey = Symbol('key'); + const obj = { [symbolKey]: 'value' }; + expect(() => guardIsKeyOfObject(obj, symbolKey)).not.toThrow(); + }); + + it('should throw an error if the symbol key does not exist', () => { + const existingSymbol = Symbol('existing'); + const missingSymbol = Symbol('missing'); + const obj = { [existingSymbol]: 'value' }; + expect(() => guardIsKeyOfObject(obj, missingSymbol)).toThrow( + 'Type guard error' + ); + }); + + it('should not throw an error if the number key exists in the object', () => { + const obj = { 1: 'one', 2: 'two' }; + expect(() => guardIsKeyOfObject(obj, 1)).not.toThrow(); + }); + + it('should throw an error if the number key does not exist in the object', () => { + const obj = { 1: 'one', 2: 'two' }; + expect(() => guardIsKeyOfObject(obj, 3)).toThrow('Type guard error'); + }); + + it('should not throw an error for empty object with no keys', () => { + const obj = {}; + expect(() => guardIsKeyOfObject(obj, 'a')).toThrow('Type guard error'); + }); + }); + describe('nonEmptyObject', () => { + it('should return true for a non-empty object', () => { + const isNonEmpty = nonEmptyObject()({ key: 'value' }); + expect(isNonEmpty).toBe(true); + }); + + it('should return false for an empty object', () => { + const isNonEmpty = nonEmptyObject()({}); + expect(isNonEmpty).toBe(false); + }); + + it('should return false for null value', () => { + const isNonEmpty = nonEmptyObject()(null); + expect(isNonEmpty).toBe(false); + }); + + it('should return false for undefined value', () => { + const isNonEmpty = nonEmptyObject()(undefined); + expect(isNonEmpty).toBe(false); + }); + + it('should return false for non-object values', () => { + const isNonEmptyString = nonEmptyObject()('string'); + expect(isNonEmptyString).toBe(false); + + const isNonEmptyNumber = nonEmptyObject()(42); + expect(isNonEmptyNumber).toBe(false); + const isNonEmptyArray = nonEmptyObject()([1, 2, 3]); + expect(isNonEmptyArray).toBe(true); + const isEmptyArray = nonEmptyObject()([]); + expect(isEmptyArray).toBe(false); + }); + }); + + describe('getValidationErrorForStrict', () => { + it('should return a validation error message for "Fields" with props', () => { + const message = getValidationErrorForStrict(['id', 'name'], 'Fields'); + expect(message).toBe( + 'Validation error: Fields should be have only props: ["id","name"]' + ); + }); + + it('should return a validation error message for "Filter" with props', () => { + const message = getValidationErrorForStrict(['age', 'gender'], 'Filter'); + expect(message).toBe( + 'Validation error: Filter should be have only props: ["age","gender"]' + ); + }); + + it('should return a message with an empty props array', () => { + const message = getValidationErrorForStrict([], 'Fields'); + expect(message).toBe( + 'Validation error: Fields should be have only props: [""]' + ); + }); + + it('should handle single prop correctly', () => { + const message = getValidationErrorForStrict(['id'], 'Fields'); + expect(message).toBe( + 'Validation error: Fields should be have only props: ["id"]' + ); + }); + + it('should handle a large props array', () => { + const props = ['a', 'b', 'c', 'd', 'e']; + const message = getValidationErrorForStrict(props, 'Filter'); + expect(message).toBe( + 'Validation error: Filter should be have only props: ["a","b","c","d","e"]' + ); + }); + }); + + describe('oneOf', () => { + it('should return true if at least one key exists in the object', () => { + const hasKey = oneOf(['key1', 'key2'])({ key1: 'value1' }); + expect(hasKey).toBe(true); + }); + + it('should return false if none of the keys exist in the object', () => { + const hasKey = oneOf(['missingKey1', 'missingKey2'])({ key1: 'value1' }); + expect(hasKey).toBe(false); + }); + + it('should return false for an empty object', () => { + const hasKey = oneOf(['key1', 'key2'])({}); + expect(hasKey).toBe(false); + }); + + it('should return false for null or undefined input', () => { + const hasKeyInNull = oneOf(['key1', 'key2'])(null); + expect(hasKeyInNull).toBe(false); + + const hasKeyInUndefined = oneOf(['key1', 'key2'])(undefined); + expect(hasKeyInUndefined).toBe(false); + }); + + it('should return false for an empty array of keys', () => { + const hasKey = oneOf([])({ key1: 'value1' }); + expect(hasKey).toBe(false); + }); + + it('should work with overlapping keys', () => { + const hasKey = oneOf(['key1', 'key2'])({ + key1: 'value1', + key2: 'value2', + }); + expect(hasKey).toBe(true); + }); + }); + + describe('stringLongerThan', () => { + it('should return true if the string is longer than the specified length', () => { + const isLongerThan = stringLongerThan(5)('testing'); + expect(isLongerThan).toBe(true); + }); + + it('should return false if the string length is equal to the specified length', () => { + const isLongerThan = stringLongerThan(7)('testing'); + expect(isLongerThan).toBe(false); + }); + + it('should return false if the string is shorter than the specified length', () => { + const isLongerThan = stringLongerThan(10)('test'); + expect(isLongerThan).toBe(false); + }); + + it('should return false for an empty string when length > 0', () => { + const isLongerThan = stringLongerThan(1)(''); + expect(isLongerThan).toBe(false); + }); + + it('should return true for length 0 with any non-empty string', () => { + const isLongerThan = stringLongerThan(0)('t'); + expect(isLongerThan).toBe(true); + }); + }); + + describe('arrayItemStringLongerThan', () => { + it('should return true if all strings in the array are longer than the specified length', () => { + const isAllLonger = arrayItemStringLongerThan(3)(['hello', 'world']); + expect(isAllLonger).toBe(true); + }); + + it('should return false if any string in the array is shorter than or equal to the specified length', () => { + const isAllLonger = arrayItemStringLongerThan(5)(['short', 'tiny']); + expect(isAllLonger).toBe(false); + }); + + it('should return false for an array with null values that fail the length check', () => { + const isAllLonger = arrayItemStringLongerThan(5)([null, 'tiny']); + expect(isAllLonger).toBe(false); + }); + + it('should return true for an array where all valid strings are longer than the specified length', () => { + const isAllLonger = arrayItemStringLongerThan(2)([null, 'hello', 'yes']); + expect(isAllLonger).toBe(true); + }); + + it('should return true if the array is empty', () => { + const isAllLonger = arrayItemStringLongerThan(3)([] as any); + expect(isAllLonger).toBe(true); + }); + + it('should return false if any string in the array is shorter and null does not interfere', () => { + const isAllLonger = arrayItemStringLongerThan(4)(['short', null, 'tiny']); + expect(isAllLonger).toBe(false); + }); + }); + + describe('stringMustBe', () => { + it('should return true for a null input', () => { + const isValid = stringMustBe()(null); + expect(isValid).toBe(true); + }); + + it('should return true for valid boolean strings', () => { + const isValidTrue = stringMustBe(TypeField.boolean)('true'); + const isValidFalse = stringMustBe(TypeField.boolean)('false'); + expect(isValidTrue).toBe(true); + expect(isValidFalse).toBe(true); + }); + + it('should return false for invalid boolean strings', () => { + const isValid = stringMustBe(TypeField.boolean)('yes'); + expect(isValid).toBe(false); + }); + + it('should return true for numeric strings', () => { + const isValid = stringMustBe(TypeField.number)('123'); + expect(isValid).toBe(true); + }); + + it('should return false for non-numeric strings when type is number', () => { + const isValid = stringMustBe(TypeField.number)('abc'); + expect(isValid).toBe(false); + }); + + it('should return true for valid ISO date strings', () => { + const isValid = stringMustBe(TypeField.date)('2023-10-10'); + expect(isValid).toBe(true); + }); + + it('should return false for invalid date strings', () => { + const isValid = stringMustBe(TypeField.date)('not-a-date'); + expect(isValid).toBe(false); + }); + + it('should return true for any input when type is string', () => { + const isValid = stringMustBe(TypeField.string)('any-value'); + expect(isValid).toBe(true); + }); + }); + + describe('elementOfArrayMustBe', () => { + it('should return true if all elements in the array are valid strings', () => { + const isValid = elementOfArrayMustBe(TypeField.string)([ + 'hello', + 'world', + ]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid string, because before converted to string', () => { + const isValid = elementOfArrayMustBe(TypeField.string)(['hello', 123]); + expect(isValid).toBe(true); + }); + + it('should return true if all elements in the array are valid numbers', () => { + const isValid = elementOfArrayMustBe(TypeField.number)([1, 2, 3]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid number', () => { + const isValid = elementOfArrayMustBe(TypeField.number)([1, 'two', 3]); + expect(isValid).toBe(false); + }); + + it('should return true if all elements are valid boolean strings', () => { + const isValid = elementOfArrayMustBe(TypeField.boolean)([ + 'true', + 'false', + ]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid boolean string', () => { + const isValid = elementOfArrayMustBe(TypeField.boolean)([ + 'true', + 'hello', + ]); + expect(isValid).toBe(false); + }); + + it('should return true for an empty array', () => { + const isValid = elementOfArrayMustBe(TypeField.string)([]); + expect(isValid).toBe(true); + }); + + it('should return true if array contains null', () => { + const isValid = elementOfArrayMustBe(TypeField.string)(['hello', null]); + expect(isValid).toBe(true); + }); + + it('should return true if all elements are valid dates', () => { + const isValid = elementOfArrayMustBe(TypeField.date)([ + '2023-01-01', + '2023-10-10', + ]); + expect(isValid).toBe(true); + }); + + it('should return false if any element is not a valid date', () => { + const isValid = elementOfArrayMustBe(TypeField.date)([ + '2023-01-01', + 'not-a-date', + ]); + expect(isValid).toBe(false); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts new file mode 100644 index 00000000..50dc7040 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.ts @@ -0,0 +1,69 @@ +import { TypeField } from '../types'; + +export const nonEmptyObject = + () => + (object: T) => + !!(typeof object === 'object' && object && Object.keys(object).length > 0); + +export const uniqueArray = () => (e: string[]) => new Set(e).size === e.length; + +export const getValidationErrorForStrict = ( + props: string[], + name: 'Fields' | 'Filter' +) => + `Validation error: ${name} should be have only props: ["${props.join( + '","' + )}"]`; + +export const oneOf = (keys: string[]) => (val: any) => { + if (!val) return false; + for (const k of keys) { + if (val[k] !== undefined) return true; + } + return false; +}; + +export const stringLongerThan = + (length = 0) => + (str: string) => + str.length > length; + +export const arrayItemStringLongerThan = + (length = 0) => + (array: [string | null, ...(string | null)[]]) => { + const checkFunction = stringLongerThan(length); + return !array.some((i) => i !== null && !checkFunction(i)); + }; + +export const stringMustBe = + (type: TypeField = TypeField.string) => + (inputString: string | null) => { + if (inputString === null) return true; + switch (type) { + case TypeField.boolean: + return inputString === 'true' || inputString === 'false'; + case TypeField.number: + return !isNaN(+inputString); + case TypeField.date: + return new Date(inputString).toString() !== 'Invalid Date'; + default: + return true; + } + }; + +export const elementOfArrayMustBe = + (type: TypeField = TypeField.string) => + (inputArray: unknown[]) => { + const checkFunc = stringMustBe(type); + return !inputArray.some((i) => !checkFunc(`${i}`)); + }; + +export function guardIsKeyOfObject( + object: R, + key: string | number | symbol +): asserts key is keyof R { + if (typeof object === 'object' && object !== null && key in object) + return void 0; + + throw new Error('Type guard error'); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts new file mode 100644 index 00000000..96438c0d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts @@ -0,0 +1,2 @@ +export const CURRENT_DATA_SOURCE_TOKEN = Symbol('CURRENT_DATA_SOURCE_TOKEN'); +export const CURRENT_ENTITY_REPOSITORY = Symbol('CURRENT_ENTITY_REPOSITORY'); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts new file mode 100644 index 00000000..403001ad --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts @@ -0,0 +1,4 @@ +export * from './di'; + +export const SUB_QUERY_ALIAS_FOR_PAGINATION = 'subQueryWithLimitOffset'; +export const ALIAS_FOR_PAGINATION = 'aliasForPagination'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts new file mode 100644 index 00000000..32f8835f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts @@ -0,0 +1,218 @@ +import { FactoryProvider } from '@nestjs/common'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { camelToKebab, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { DataSource, EntityManager, Repository } from 'typeorm'; + +import { + CURRENT_DATA_SOURCE_TOKEN, + CURRENT_ENTITY_REPOSITORY, +} from '../constants'; +import { + CURRENT_ENTITY_MANAGER_TOKEN, + FIND_ONE_ROW_ENTITY, + CHECK_RELATION_NAME, + PARAMS_FOR_ZOD_SCHEMA, + ORM_SERVICE, + FIELD_FOR_ENTITY, + RUN_IN_TRANSACTION_FUNCTION, + GLOBAL_MODULE_OPTIONS_TOKEN, +} from '../../../constants'; +import { + EntityProps, + FieldWithType, + FindOneRowEntity, + CheckRelationNme, + ZodParams, + GetFieldForEntity, +} from '../../mixin/types'; +import { + ObjectLiteral, + EntityTarget, + ResultGeneralParam, + RequiredFromPartial, + ConfigParam, + RunInTransaction, +} from '../../../types'; +import { + getField, + getPropsTreeForRepository, + getArrayPropsForEntity, + getTypeForAllProps, + getRelationTypeArray, + getTypePrimaryColumn, + getFieldWithType, + getPropsFromDb, + getRelationTypeName, + getRelationTypePrimaryColumn, +} from '../orm-helper'; + +import { TypeOrmService, TypeormUtilsService } from '../service'; +import { getEntityName } from '../../mixin/helper'; +import { TypeOrmModule } from '@klerick/json-api-nestjs'; +import { TypeOrmParam } from '../type'; + +export function CurrentDataSourceProvider( + connectionName?: string +): FactoryProvider { + return { + provide: CURRENT_DATA_SOURCE_TOKEN, + useFactory: (dataSource: DataSource) => dataSource, + inject: [getDataSourceToken(connectionName)], + }; +} + +export function CurrentEntityManager(): FactoryProvider { + return { + provide: CURRENT_ENTITY_MANAGER_TOKEN, + useFactory: (dataSource: DataSource) => dataSource.manager, + inject: [CURRENT_DATA_SOURCE_TOKEN], + }; +} + +export function GetFieldForEntity(): FactoryProvider< + GetFieldForEntity +> { + return { + provide: FIELD_FOR_ENTITY, + useFactory: (entityManager: EntityManager) => { + return (entity: EntityTarget) => + getField(entityManager.getRepository(entity)); + }, + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + }; +} + +export function CurrentEntityRepository( + entity: E +): FactoryProvider> { + return { + provide: CURRENT_ENTITY_REPOSITORY, + useFactory: (entityManager: EntityManager) => + entityManager.getRepository(entity as unknown as EntityTarget), + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + }; +} + +export function ZodParamsFactory(): FactoryProvider< + ZodParams> +> { + return { + provide: PARAMS_FOR_ZOD_SCHEMA, + inject: [CURRENT_ENTITY_REPOSITORY], + useFactory: (repo: Repository) => { + const primaryColumns = repo.metadata.primaryColumns[0] + .propertyName as EntityProps; + const fieldWithType = ObjectTyped.entries(getFieldWithType(repo)) + .filter(([key]) => key !== repo.metadata.primaryColumns[0].propertyName) + .reduce( + (acum, [key, type]) => ({ + ...acum, + [key]: type, + }), + {} as FieldWithType + ); + + return { + entityFieldsStructure: getField(repo), + entityRelationStructure: getPropsTreeForRepository(repo), + propsArray: getArrayPropsForEntity(repo), + propsType: getTypeForAllProps(repo), + typeId: getTypePrimaryColumn(repo), + typeName: camelToKebab(getEntityName(repo.target)), + fieldWithType, + propsDb: getPropsFromDb(repo), + primaryColumn: primaryColumns, + relationArrayProps: getRelationTypeArray(repo), + relationPopsName: getRelationTypeName(repo), + primaryColumnType: getRelationTypePrimaryColumn(repo), + } satisfies ZodParams>; + }, + }; +} + +export function FindOneRowEntityFactory< + E extends ObjectLiteral +>(): FactoryProvider> { + return { + provide: FIND_ONE_ROW_ENTITY, + inject: [CURRENT_ENTITY_REPOSITORY, TypeormUtilsService], + useFactory: ( + repository: Repository, + typeormUtilsService: TypeormUtilsService + ) => { + return async (entity, value) => { + const params = 'params'; + return await repository + .createQueryBuilder(typeormUtilsService.currentAlias) + .where( + `${typeormUtilsService.getAliasPath( + typeormUtilsService.currentPrimaryColumn + )} = :${params}` + ) + .setParameters({ + [params]: value, + }) + .getOne(); + }; + }, + }; +} + +export function CheckRelationNameFactory< + E extends ObjectLiteral +>(): FactoryProvider> { + return { + provide: CHECK_RELATION_NAME, + inject: [TypeormUtilsService], + useFactory(typeormUtilsService: TypeormUtilsService) { + return (entity, value) => + !!typeormUtilsService.relationFields.find((i) => i === value); + }, + }; +} + +export function RunInTransactionFactory(): FactoryProvider { + return { + provide: RUN_IN_TRANSACTION_FUNCTION, + inject: [GLOBAL_MODULE_OPTIONS_TOKEN, CURRENT_DATA_SOURCE_TOKEN], + useFactory( + options: ResultGeneralParam & { + type: typeof TypeOrmModule; + options: RequiredFromPartial; + }, + dataSource: DataSource + ) { + const { + options: { runInTransaction }, + } = options; + + if (runInTransaction && typeof runInTransaction === 'function') { + return (callback) => + runInTransaction('READ COMMITTED', () => callback()); + } + + return async (callback) => { + const queryRunner = dataSource.createQueryRunner(); + await queryRunner.startTransaction('READ UNCOMMITTED'); + let result: unknown; + try { + result = await callback(); + await queryRunner.commitTransaction(); + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + return result; + }; + }, + }; +} + +export function OrmServiceFactory() { + return { + provide: ORM_SERVICE, + useClass: TypeOrmService, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts new file mode 100644 index 00000000..e9cdcce4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts @@ -0,0 +1,2 @@ +export * from './type-orm.module'; +export * from './type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts new file mode 100644 index 00000000..80541044 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts @@ -0,0 +1,283 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { + ObjectTyped, + EntityRelation, + TypeOfArray, + EntityProps, +} from '@klerick/json-api-nestjs-shared'; +import { Repository } from 'typeorm'; +import { IMemoryDb } from 'pg-mem'; + +import { + mockDBTestModule, + createAndPullSchemaBase, + pullUser, + pullAllData, + providerEntities, + getRepository, + Users, + Addresses, + Notes, + Comments, + Roles, + UserGroups, +} from '../../../mock-utils'; + +import { + getField, + getPropsTreeForRepository, + fromRelationTreeToArrayName, + getArrayFields, + getArrayPropsForEntity, + getFieldWithType, + getTypeForAllProps, + getRelationTypeArray, + getTypePrimaryColumn, + getPropsFromDb, + getRelationTypeName, + getRelationTypePrimaryColumn, +} from './'; + +import { PropsArray, ArrayPropsForEntity, TypeField } from '../../mixin/types'; + +describe('type-orm-helper', () => { + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + let db: IMemoryDb; + let user: Users; + let userWithRelation: Users; + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [...providerEntities(getDataSourceToken())], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + + user = await pullUser(userRepository); + userWithRelation = await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + }); + + it('getField', async () => { + const { relations, field } = getField(userRepository); + const userFieldProps = Object.getOwnPropertyNames( + user + ) as EntityProps[]; + const hasUserFieldInResultField = userFieldProps.some( + (field) => !field.includes(field) + ); + + const hasResultInUserField = field.some( + (field) => !userFieldProps.includes(field) + ); + + const userRelationProps: EntityRelation[] = ( + Object.getOwnPropertyNames(userWithRelation) as (EntityProps & + EntityRelation)[] + ).filter((props) => !userFieldProps.includes(props)); + + const hasUserRelationInResultField = userRelationProps.some( + (field) => !relations.includes(field) + ); + + const hasResultInUserRelation = relations.some( + (field) => !userRelationProps.includes(field) + ); + + expect(hasUserFieldInResultField).toEqual(false); + expect(hasResultInUserField).toEqual(false); + + expect(hasUserRelationInResultField).toEqual(false); + expect(hasResultInUserRelation).toEqual(false); + }); + + it('getPropsTreeForRepository', () => { + const relationField = getPropsTreeForRepository(userRepository); + const userFieldProps = Object.getOwnPropertyNames( + user + ) as EntityProps[]; + const userRelationProps: EntityRelation[] = ( + Object.getOwnPropertyNames(userWithRelation) as (EntityProps & + EntityRelation)[] + ).filter((props) => !userFieldProps.includes(props)); + + const hasUserRelationInResultField = userRelationProps.some( + (field) => !Object.keys(relationField).includes(field) + ); + const hasResultInUserRelation = ObjectTyped.keys(relationField).some( + (field) => !userRelationProps.includes(field) + ); + expect(hasUserRelationInResultField).toEqual(false); + expect(hasResultInUserRelation).toEqual(false); + + for (const [relationName, fieldsRelation] of ObjectTyped.entries( + relationField + )) { + const check = fieldsRelation.some((field) => { + const targetItem = userWithRelation[relationName]; + const target = Array.isArray(targetItem) ? targetItem[0] : targetItem; + // @ts-ignore + return !ObjectTyped.keys(target).includes(field); + }); + expect(check).toEqual(false); + } + }); + + it('fromRelationTreeToArrayName', () => { + const { relations, field } = getField(userRepository); + + const relationField = getPropsTreeForRepository(userRepository); + const checkArray = fromRelationTreeToArrayName(relationField); + + for (const key of relations) { + let resultKey = + key === 'manager' ? 'Users' : key === 'userGroup' ? 'UserGroups' : key; + + const relationsRepo = + userRepository.metadata.connection.getRepository< + TypeOfArray + >(resultKey); + const { field: relationsFields } = getField(relationsRepo); + const textField = relationsFields.map((r) => `${key}.${r}`); + const check = textField.some((i) => !checkArray.includes(i as any)); + expect(check).toEqual(false); + } + }); + + it('getArrayFields', () => { + const result = getArrayFields(addressesRepository); + expect(result).toEqual({ + arrayField: true, + } as PropsArray); + }); + + it('getArrayPropsForEntity', () => { + const result = getArrayPropsForEntity(userRepository); + const check: ArrayPropsForEntity = { + target: { + testReal: true, + testArrayNull: true, + }, + manager: { + testReal: true, + testArrayNull: true, + }, + comments: {}, + notes: {}, + userGroup: {}, + roles: {}, + addresses: { + arrayField: true, + }, + }; + expect(result).toEqual(check); + }); + + it('getFieldWithType', () => { + const result = getFieldWithType(addressesRepository); + expect(result.arrayField).toBe('array'); + expect(result.state).toBe('string'); + expect(result.id).toBe('number'); + expect(result.createdAt).toBe('date'); + const result2 = getFieldWithType(userRepository); + + expect(result2.isActive).toBe('boolean'); + }); + + it('getRelationType', () => { + const result = getRelationTypeArray(userRepository); + expect(result.roles).toBe(true); + expect(result.comments).toBe(true); + expect(result.manager).toBe(false); + expect(result.addresses).toBe(false); + expect(result.userGroup).toBe(false); + expect(result.notes).toBe(true); + }); + + it('getRelationTypeName', () => { + const result = getRelationTypeName(userRepository); + expect(result.roles).toBe('Roles'); + expect(result.comments).toBe('Comments'); + expect(result.manager).toBe('Users'); + expect(result.addresses).toBe('Addresses'); + expect(result.userGroup).toBe('UserGroups'); + expect(result.notes).toBe('Notes'); + }); + + it('getRelationTypePrimaryColumn', () => { + const result = getRelationTypePrimaryColumn(userRepository); + expect(result.roles).toBe(TypeField.number); + expect(result.comments).toBe(TypeField.number); + expect(result.manager).toBe(TypeField.number); + expect(result.addresses).toBe(TypeField.number); + expect(result.userGroup).toBe(TypeField.number); + expect(result.notes).toBe(TypeField.string); + }); + + it('getTypePrimaryColumn', () => { + expect(getTypePrimaryColumn(userRepository)).toBe(TypeField.number); + expect(getTypePrimaryColumn(notesRepository)).toBe(TypeField.string); + }); + // + // it('getPrimaryColumnsForRelation', () => { + // const result = getPrimaryColumnsForRelation(userRepository); + // expect(result.roles).toBe('id'); + // expect(result.manager).toBe('id'); + // }); + // + // it('geTisArrayRelation', () => { + // const result = getIsArrayRelation(userRepository); + // expect(result).toEqual({ + // addresses: false, + // manager: false, + // roles: true, + // comments: true, + // notes: true, + // userGroup: false, + // }); + // }); + + it('getTypeForAllProps', () => { + const result = getTypeForAllProps(userRepository); + expect(result.manager.id).toBe(TypeField.number); + expect(result.testDate).toBe(TypeField.date); + expect(result.comments.id).toBe(TypeField.number); + expect(result.notes.id).toBe(TypeField.string); + }); + + it('getPropsFromDb', () => { + const result = getPropsFromDb(userRepository); + // testReal has isNullable false but have default should be true + expect(result['testReal']).toEqual({ + type: 'real', + isArray: true, + isNullable: true, + }); + + const result2 = getPropsFromDb(rolesRepository); + expect(result2['key']).toEqual({ + type: 'varchar', + isArray: false, + isNullable: false, + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts new file mode 100644 index 00000000..f6e67ffa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts @@ -0,0 +1,294 @@ +import { EntityProps, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { Repository } from 'typeorm'; + +import { ObjectLiteral } from '../../../types'; +import { + RelationTree, + ValueOf, + UnionToTuple, + TypeCast, + Concat, + TypeOfArray, + CastProps, + PropsNameResultField, + PropsArray, + RelationType, + ResultGetField, + ArrayPropsForEntity, + AllFieldWithType, + TypeField, + FieldWithType, + RelationPropsArray, + TypeForId, + PropsForField, + ColumnType, + RelationPropsTypeName, + RelationPrimaryColumnType, +} from '../../mixin/types'; +import { getEntityName } from '../../mixin/helper'; + +export type ConcatFieldWithRelation< + R extends string, + T extends readonly string[] +> = ValueOf<{ + [K in T[number]]: Concat; +}>; + +export type ConcatRelationUnion< + E extends ObjectLiteral, + R = RelationTree +> = ValueOf<{ + [K in keyof R]: ConcatFieldWithRelation< + TypeCast, + TypeCast + >; +}>; + +export type ConcatRelation = TypeCast< + UnionToTuple>, + [string, ...string[]] +>; + +export const getField = ( + repository: Repository +): ResultGetField => { + const relations = repository.metadata.relations.map((i) => { + return i.propertyName; + }); + + const field = repository.metadata.columns + .filter((i) => !relations.includes(i.propertyName)) + .map((r) => r.propertyName); + + return { + field, + relations, + } as unknown as ResultGetField; +}; + +export const fromRelationTreeToArrayName = ( + relationTree: RelationTree +): ConcatRelation => { + return ObjectTyped.entries(relationTree).reduce((acum, [name, filed]) => { + acum.push(...filed.map((i) => `${name.toLocaleString()}.${i}`)); + return acum; + }, [] as string[]) as unknown as ConcatRelation; +}; + +export const getPropsTreeForRepository = ( + repository: Repository +): RelationTree => { + const dataSource = repository.metadata.connection; + + const relationType = repository.metadata.relations.reduce((acum, i) => { + acum[i.propertyName] = i.inverseEntityMetadata.target; + return acum; + }, {} as Record) as unknown as RelationType; + + return ObjectTyped.entries(relationType).reduce( + (acum, [key, value]) => ({ + ...acum, + ...{ [key]: getField(dataSource.getRepository(value))['field'] }, + }), + {} as RelationTree + ); +}; + +export type PropertyTarget< + E extends ObjectLiteral, + For extends PropsNameResultField +> = { + [K in ResultGetField[For][number]]: K extends keyof E + ? TypeOfArray + : never; +}; + +export function guardKeyForPropertyTarget< + E extends ObjectLiteral, + For extends PropsNameResultField, + R extends PropertyTarget +>(relationsTargets: R, key: any): asserts key is keyof R { + if (!(key in relationsTargets)) throw new Error('Type guard error'); +} + +export const getArrayPropsForEntity = ( + repository: Repository +): ArrayPropsForEntity => { + const connection = repository.metadata.connection; + + const relationsTargets = repository.metadata.relations.reduce( + (acum, i) => ({ + ...acum, + [i.propertyName]: i.type, + }), + {} as Record + ) as PropertyTarget; + + const { relations } = getField(repository); + const relationsArrayFields = relations.reduce( + (acum, item) => { + guardKeyForPropertyTarget(relationsTargets, item); + const target = relationsTargets[item] as TypeCast< + TypeOfArray>, + ObjectLiteral + >; + + const repository = connection.getRepository( + target as Function + ) as Repository; + + acum[item] = getArrayFields(repository) as PropsArray< + TypeOfArray> + >; + + return acum; + }, + {} as { + [K in ResultGetField['relations'][number]]: PropsArray< + TypeOfArray> + >; + } + ); + + return { + target: getArrayFields(repository), + ...relationsArrayFields, + }; +}; + +export const getArrayFields = ( + repository: Repository +): PropsArray => { + const relations = repository.metadata.relations.map((i) => { + return i.propertyName; + }); + + return repository.metadata.columns + .filter((i) => !relations.includes(i.propertyName)) + .reduce((acum, metaData) => { + if (metaData.isArray) { + acum[metaData.propertyName] = true; + } + return acum; + }, {} as Record) as PropsArray; +}; + +export const getTypeForAllProps = ( + repository: Repository +): AllFieldWithType => { + const targetField = getFieldWithType(repository); + + const relationField = repository.metadata.relations.reduce((acum, item) => { + acum[item.propertyName] = getFieldWithType( + repository.manager.getRepository(item.inverseEntityMetadata.target) + ); + return acum; + }, {} as any); + + return { + ...targetField, + ...relationField, + }; +}; + +export const getFieldWithType = ( + repository: Repository +): FieldWithType => { + const { field } = getField(repository); + + const entity = repository.target as any; + const result = {} as any; + for (const item of field) { + let typeProps: TypeField = TypeField.string; + switch (Reflect.getMetadata('design:type', entity['prototype'], item)) { + case Array: + typeProps = TypeField.array; + break; + case Date: + typeProps = TypeField.date; + break; + case Number: + typeProps = TypeField.number; + break; + case Boolean: + typeProps = TypeField.boolean; + break; + case Object: + typeProps = TypeField.object; + break; + default: + typeProps = TypeField.string; + } + result[item] = typeProps; + } + + return result; +}; + +export const getRelationTypeArray = ( + repository: Repository +): RelationPropsArray => { + const { relations } = getField(repository); + + const entity = repository.target as any; + const result = {} as any; + for (const item of relations) { + result[item] = + Reflect.getMetadata('design:type', entity['prototype'], item) === Array; + } + return result; +}; + +export const getTypePrimaryColumn = ( + repository: Repository +): TypeForId => { + const target = repository.target as any; + const primaryColumn = repository.metadata.primaryColumns[0].propertyName; + + return Reflect.getMetadata( + 'design:type', + target['prototype'], + primaryColumn + ) === Number + ? TypeField.number + : TypeField.string; +}; + +export const getPropsFromDb = ( + repository: Repository +): PropsForField => { + return repository.metadata.columns.reduce((acum, i) => { + const tmp = i.propertyName as unknown as EntityProps; + acum[tmp] = { + type: i.type as ColumnType, + isArray: i.isArray, + isNullable: i.isNullable || i.default !== undefined, + }; + return acum; + }, {} as PropsForField); +}; + +export const getRelationTypeName = ( + repository: Repository +): RelationPropsTypeName => { + return repository.metadata.relations.reduce((acum, i) => { + acum[i.propertyName] = getEntityName(i.inverseEntityMetadata.target); + return acum; + }, {} as Record) as RelationPropsTypeName; +}; + +export const getRelationTypePrimaryColumn = ( + repository: Repository +): RelationPrimaryColumnType => { + return repository.metadata.relations.reduce((acum, i) => { + const target = i.inverseEntityMetadata.target as any; + const primaryColumn = + i.inverseEntityMetadata.primaryColumns[0].propertyName; + acum[i.propertyName] = + Reflect.getMetadata('design:type', target['prototype'], primaryColumn) === + Number + ? TypeField.number + : TypeField.string; + return acum; + }, {} as Record) as RelationPrimaryColumnType; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts new file mode 100644 index 00000000..4f730de9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts @@ -0,0 +1,71 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; + +import { + createAndPullSchemaBase, + mockDBTestModule, + providerEntities, +} from '../../../../mock-utils'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; + +import { getRepository, pullUser, Users } from '../../../../mock-utils'; + +import { Repository } from 'typeorm'; +import { CONTROL_OPTIONS_TOKEN, ORM_SERVICE } from '../../../../constants'; + +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('deleteOne', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let user: Users; + let userRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ userRepository } = getRepository(module)); + user = await pullUser(userRepository); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + }); + + it('Should be ok', async () => { + await typeormService.deleteOne(`${user.id}`); + expect(await userRepository.findOneBy({ id: user.id })).toBe(null); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts new file mode 100644 index 00000000..1a6871d7 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.ts @@ -0,0 +1,21 @@ +import { FindOptionsWhere } from 'typeorm'; +import { TypeOrmService } from '../../service'; +import { ObjectLiteral } from '../../../../types'; + +export async function deleteOne( + this: TypeOrmService, + id: number | string +): Promise { + const data = await this.repository.findOne({ + where: { + [this.typeormUtilsService.currentPrimaryColumn.toString()]: id, + } as FindOptionsWhere, + }); + if (!data) return void 0; + + this.config.useSoftDelete + ? await this.repository.softRemove(data) + : await this.repository.remove(data); + + return void 0; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts new file mode 100644 index 00000000..5a83d3fc --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts @@ -0,0 +1,190 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; + +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('deleteRelationship', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + let typeormUtilsService: TypeormUtilsService; + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + typeormUtilsService = + module.get>(TypeormUtilsService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Should be ok', async () => { + const checkUser = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + + const roles = await rolesRepository.find(); + const userGroups = await userGroupRepository.find(); + const users = await userRepository.find(); + + if (!checkUser) { + throw new Error('not found mock'); + } + + const userGroupData = { + type: 'user-groups', + id: userGroups + .find((i) => checkUser.userGroup.id === i.id) + ?.id.toString(), + }; + const rolesData = [ + { + type: 'roles', + id: roles + .find((i) => checkUser.roles.find((a) => a.id === i.id)) + ?.id.toString(), + }, + ]; + + const managerData = { + type: 'users', + id: users.find((i) => checkUser.manager.id === i.id)?.id.toString(), + }; + await typeormService.deleteRelationship(1, 'roles', rolesData as any); + await typeormService.deleteRelationship( + 1, + 'userGroup', + userGroupData as any + ); + await typeormService.deleteRelationship(1, 'manager', managerData as any); + + const checkUserAfterPost = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + if (!checkUserAfterPost) { + throw new Error('not found'); + } + expect(checkUserAfterPost.manager).toBe(null); + expect(checkUserAfterPost.roles.map((i) => i.id.toString()).sort()).toEqual( + checkUser.roles + .map((i) => i.id.toString()) + .filter((i) => !rolesData.map((i) => i.id).includes(i)) + .sort() + ); + expect(checkUserAfterPost.userGroup).toBe(null); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts new file mode 100644 index 00000000..141b41a8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts @@ -0,0 +1,32 @@ +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; + +import { PostRelationshipData } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; + +export async function deleteRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: TypeOrmService, + id: number | string, + rel: Rel, + input: PostRelationshipData +): Promise { + const idsResult = await this.typeormUtilsService.validateRelationInputData( + rel, + input + ); + const postBuilder = this.repository + .createQueryBuilder() + .relation(rel.toString()) + .of(id); + + if (Array.isArray(idsResult)) { + await postBuilder.remove(idsResult); + } else { + await postBuilder.set(null); + } + return void 0; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts new file mode 100644 index 00000000..45500dc3 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts @@ -0,0 +1,406 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { Equal, IsNull, Repository } from 'typeorm'; +import { IMemoryDb } from 'pg-mem'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, + DEFAULT_QUERY_PAGE, + DEFAULT_PAGE_SIZE, +} from '../../../../constants'; +import { ObjectLiteral as Entity } from '../../../../types'; + +import { Query } from '../../../mixin/zod'; + +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +function getDefaultQuery() { + const filter = { + relation: null, + target: null, + }; + const defaultQuery: Query = { + [QueryField.filter]: filter, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }, + }; + + return defaultQuery; +} + +describe('getAll', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('order', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + + const checkData = await userRepository.find({ + relations: { + addresses: true, + comments: true, + }, + order: { + id: 'DESC', + comments: { + id: 'DESC', + }, + }, + }); + + const query = getDefaultQuery(); + query.include = ['addresses', 'comments']; + query.sort = { + target: { + id: 'DESC', + }, + comments: { + id: 'DESC', + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith(checkData); + }); + + it('include', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + + const checkData = await userRepository.findOne({ + where: { + id: 1, + }, + relations: { + addresses: true, + comments: true, + }, + }); + + const query = getDefaultQuery(); + query.include = ['addresses', 'comments']; + query.filter.target = { + id: { + eq: `${checkData?.id}`, + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + it('select', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + + const checkData = await userRepository.findOne({ + select: { + id: true, + isActive: true, + addresses: { + state: true, + id: true, + }, + comments: { + text: true, + id: true, + }, + }, + where: { + id: 1, + }, + relations: { + addresses: true, + comments: true, + }, + }); + + const query = getDefaultQuery(); + query.fields = { + target: ['id', 'isActive'], + addresses: ['state'], + comments: ['text'], + }; + query.include = ['addresses', 'comments']; + query.filter.target = { + id: { + eq: `${checkData?.id}`, + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + describe('filter', () => { + let firstRole: Roles; + let secondRole: Roles; + let addresses: Addresses[]; + let comments: Comments[]; + beforeAll(async () => { + firstRole = (await rolesRepository.findOneBy({ + id: 1, + })) as Roles; + secondRole = (await rolesRepository.findOneBy({ + id: 2, + })) as Roles; + + addresses = await addressesRepository.find(); + comments = await commentsRepository.find(); + }); + + it('Target props with null', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + + const query = getDefaultQuery(); + query.filter.target = { + id: { eq: '1' }, + firstName: { eq: null }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toHaveBeenCalledTimes(0); + }); + + it('Target props', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.findOne({ + where: { + id: 1, + }, + }); + const query = getDefaultQuery(); + query.filter.target = { + id: { eq: `${checkData?.id}` }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + it('Check relation with the same Entity', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.findOne({ + where: { + id: 1, + comments: { + text: Equal(comments[0].text), + }, + }, + relations: { + comments: true, + }, + }); + const query = getDefaultQuery(); + query.filter.relation = { + comments: { + text: { + eq: comments[0].text, + }, + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + // it('Target relation is null', async () => { + // const query = getDefaultQuery(); + // query.filter.target = { + // comments: { + // eq: 'null', + // }, + // }; + // await typeormService.getAll(query); + // }); + + it('Relation many-to-one', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.findOne({ + where: { + id: 1, + }, + relations: { + manager: true, + }, + }); + + const query = getDefaultQuery(); + query.filter.target = { + id: { + eq: '1', + }, + }; + query.filter.relation = { + manager: { + id: { + eq: '2', + }, + }, + }; + query.include = ['manager']; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + it('Relation one-to-many', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.findOne({ + where: { + id: 1, + addresses: { + state: Equal(addresses[0].state), + }, + }, + relations: { + addresses: true, + }, + }); + const query = getDefaultQuery(); + query.filter.relation = { + addresses: { + state: { + eq: addresses[0].state, + }, + }, + }; + await typeormService.getAll(query); + expect(spyOnTransformData).toBeCalledWith([checkData]); + }); + + it('Relation many-to-many', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const checkData = await userRepository.find({ + where: { + id: 1, + roles: { + name: Equal(firstRole.name), + }, + }, + relations: { + roles: true, + }, + }); + + const query = getDefaultQuery(); + query.include = ['roles']; + query.filter.relation = { + roles: { + name: { + eq: firstRole.name, + }, + }, + }; + const { data } = await typeormService.getAll(query); + expect(spyOnTransformData).not.toBeCalled(); + expect(data).toEqual([]); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts new file mode 100644 index 00000000..2fe92c5e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts @@ -0,0 +1,264 @@ +import { ObjectTyped, ResourceObject } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; +import { Query } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; +import { TupleOfEntityRelation } from '../../../mixin/types'; +import { ASC, DESC } from '../../../../constants'; + +import { + ALIAS_FOR_PAGINATION, + SUB_QUERY_ALIAS_FOR_PAGINATION, +} from '../../constants'; + +type OrderByCondition = Record; + +function getSortObject(params: any, relationName: string) { + return Object.entries(params).reduce((acum, [props, sort]) => { + acum[`${relationName}.${props}`] = `${sort}` === ASC ? ASC : DESC; + return acum; + }, {} as OrderByCondition); +} + +export async function getAll( + this: TypeOrmService, + query: Query +): Promise> { + const { fields, filter, include, sort, page } = query; + + let defaultSortObject: OrderByCondition = { + [`${ + this.typeormUtilsService.currentAlias + }.${this.typeormUtilsService.currentPrimaryColumn.toString()}`]: ASC, + }; + + const includeForCountQuery = new Set(); + const selectFields = new Set(); + const includeRel = new Set(); + + const skip = (page.number - 1) * page.size; + + const expressionArrayForTarget = + this.typeormUtilsService.getFilterExpressionForTarget(query); + const expressionArrayForRelation = + this.typeormUtilsService.getFilterExpressionForRelation(query); + const expressionArray = [ + ...expressionArrayForTarget, + ...expressionArrayForRelation, + ]; + + if (sort) { + const { target, ...relation } = sort; + const targetOrder = getSortObject( + target || {}, + this.typeormUtilsService.currentAlias + ); + + const relOrder = Object.entries(relation || {}).reduce( + (acum, [name, order]) => { + return { + ...acum, + ...getSortObject( + order || {}, + this.typeormUtilsService.getAliasForRelation(name) + ), + }; + }, + {} as OrderByCondition + ); + const resultOrder = { + ...targetOrder, + ...relOrder, + }; + if (Object.keys(resultOrder).length > 0) { + defaultSortObject = resultOrder; + } + for (const item of ObjectTyped.keys(relation)) { + includeForCountQuery.add(item.toString()); + } + } + + const queryBuilderForCount = this.repository + .createQueryBuilder(this.typeormUtilsService.currentAlias) + .select( + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + ), + this.typeormUtilsService.currentPrimaryColumn.toString() + ) + .orderBy(defaultSortObject); + + for (const i in expressionArray) { + const { params, alias, selectInclude, expression } = expressionArray[i]; + const expressionTempArray: string[] = []; + if (alias) { + expressionTempArray.push(alias); + } + expressionTempArray.push(expression); + queryBuilderForCount[i === '0' ? 'where' : 'andWhere']( + expressionTempArray.join(' ') + ); + if (params) { + if (Array.isArray(params)) { + for (const { name, val } of params) { + queryBuilderForCount.setParameters({ [name]: val }); + } + } else { + queryBuilderForCount.setParameters({ [params.name]: params.val }); + } + } + if (selectInclude) includeForCountQuery.add(selectInclude); + } + + for (const rel of [...includeForCountQuery]) { + const currentIncludeAlias = + this.typeormUtilsService.getAliasForRelation(rel); + queryBuilderForCount.leftJoin( + this.typeormUtilsService.getAliasPath(rel), + currentIncludeAlias + ); + } + + const count = await queryBuilderForCount.getCount(); + const meta = { + pageNumber: page.number, + totalItems: count, + pageSize: page.size, + }; + + if (count === 0) { + return { + meta, + data: [], + }; + } + + const aliasForIdResultPagination = this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn, + ALIAS_FOR_PAGINATION, + '_' + ); + + const resultIds = await this.repository + .createQueryBuilder(ALIAS_FOR_PAGINATION) + .select( + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn, + ALIAS_FOR_PAGINATION + ), + aliasForIdResultPagination + ) + .innerJoin( + `(${queryBuilderForCount.offset(skip).limit(page.size).getQuery()})`, + SUB_QUERY_ALIAS_FOR_PAGINATION, + `${this.typeormUtilsService.getAliasPath( + queryBuilderForCount.escape( + this.typeormUtilsService.currentPrimaryColumn.toString() + ), + queryBuilderForCount.escape(SUB_QUERY_ALIAS_FOR_PAGINATION) + )} = ${this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn, + ALIAS_FOR_PAGINATION + )}` + ) + .setParameters(queryBuilderForCount.getParameters()) + .getRawMany<{ + [K: typeof aliasForIdResultPagination]: number; + }>(); + + const ids = resultIds.map((i) => i[aliasForIdResultPagination]); + if (ids.length === 0) { + return { + meta, + data: [], + }; + } + if (include) { + for (const rel of include) { + includeRel.add(rel); + } + } + + if (fields) { + if (include) { + for (const rel of include) { + const currentIncludeAlias = + this.typeormUtilsService.getAliasForRelation(rel); + const primaryColumnName = + this.typeormUtilsService.getPrimaryColumnForRel(rel); + selectFields.add(`${currentIncludeAlias}.${primaryColumnName}`); + } + } + + const { target, ...other } = fields; + if (target) { + for (const item of target) { + selectFields.add(`${this.typeormUtilsService.currentAlias}.${item}`); + } + } + + for (const [rel, fields] of ObjectTyped.entries(other)) { + const currentIncludeAlias = this.typeormUtilsService.getAliasForRelation( + rel as TupleOfEntityRelation[number] + ); + if (!fields) continue; + for (const field of fields) { + selectFields.add(`${currentIncludeAlias.toString()}.${field}`); + } + } + } + + const resultQuery = this.repository + .createQueryBuilder() + .orderBy(defaultSortObject); + + if (selectFields.size > 0) { + resultQuery.select([...selectFields]); + } + + resultQuery.whereInIds(ids); + for (const expressionItem of expressionArrayForRelation) { + const { selectInclude, alias, paramsForResult, params, expression } = + expressionItem; + if (paramsForResult) { + for (const item of paramsForResult) { + resultQuery.andWhere(item); + } + } else { + resultQuery.andWhere(`${alias} ${expression}`); + } + + if (params) { + if (Array.isArray(params)) { + for (const item of params) { + resultQuery.setParameters({ [item.name]: item.val }); + } + } else { + resultQuery.setParameters({ [params.name]: params.val }); + } + } + if (selectInclude) includeRel.add(selectInclude); + } + + for (const item of [...includeRel]) { + const currentIncludeAlias = + this.typeormUtilsService.getAliasForRelation(item); + if (!currentIncludeAlias) continue; + resultQuery[selectFields.size > 0 ? 'leftJoin' : 'leftJoinAndSelect']( + this.typeormUtilsService.getAliasPath(item), + currentIncludeAlias + ); + } + const resultData = await resultQuery.getMany(); + const { included, data } = + this.transformDataService.transformData(resultData); + return { + meta: { + pageNumber: page.number, + totalItems: count, + pageSize: page.size, + }, + data, + ...(included ? { included } : {}), + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts new file mode 100644 index 00000000..d0a6adaa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts @@ -0,0 +1,191 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { IMemoryDb } from 'pg-mem'; +import { Equal, Repository } from 'typeorm'; + +import { ObjectLiteral as Entity } from '../../../../types'; +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + DEFAULT_PAGE_SIZE, + DEFAULT_QUERY_PAGE, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { Query } from '../../../mixin/zod'; +import { NotFoundException } from '@nestjs/common'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +function getDefaultQuery() { + const defaultQuery: Query = { + [QueryField.filter]: { + relation: null, + target: null, + }, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: DEFAULT_PAGE_SIZE, + number: DEFAULT_QUERY_PAGE, + }, + }; + + return defaultQuery; +} + +describe('getOne', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Get one item', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const query = getDefaultQuery(); + const checkData = await userRepository.findOne({ + where: { + id: Equal(1), + }, + relations: { + addresses: true, + comments: true, + }, + }); + query.include = ['addresses', 'comments']; + await typeormService.getOne('1', query); + expect(spyOnTransformData).toBeCalledWith(checkData); + }); + it('Get one item with select', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'transformData' + ); + const query = getDefaultQuery(); + const checkData = await userRepository.findOne({ + select: { + firstName: true, + id: true, + isActive: true, + comments: { + id: true, + text: true, + }, + addresses: { + id: true, + }, + manager: { + id: true, + login: true, + }, + }, + where: { + id: Equal(1), + }, + relations: { + addresses: true, + comments: true, + manager: true, + }, + }); + query.include = ['addresses', 'comments', 'manager']; + query.fields = { + target: ['firstName', 'isActive'], + comments: ['text'], + manager: ['login'], + }; + await typeormService.getOne('1', query); + expect(spyOnTransformData).toBeCalledWith(checkData); + }); + it('Should be error', async () => { + expect.assertions(1); + try { + const query = getDefaultQuery(); + await typeormService.getOne('1000000', query); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts new file mode 100644 index 00000000..257ef7cb --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts @@ -0,0 +1,95 @@ +import { NotFoundException } from '@nestjs/common'; +import { ObjectTyped, ResourceObject } from '@klerick/json-api-nestjs-shared'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { QueryOne } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; + +export async function getOne( + this: TypeOrmService, + id: number | string, + query: QueryOne +): Promise> { + const { include, fields } = query; + const selectFields = new Set(); + const builder = this.repository.createQueryBuilder( + this.typeormUtilsService.currentAlias + ); + + if (fields) { + selectFields.add( + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + ) + ); + + const { target, ...other } = fields; + if (target) { + for (const fieldItem of target) { + selectFields.add(this.typeormUtilsService.getAliasPath(fieldItem)); + } + } + + for (const [rel, fieldRel] of ObjectTyped.entries(other)) { + if (fieldRel) { + for (const itemFieldRel of fieldRel) { + selectFields.add( + this.typeormUtilsService.getAliasPath( + itemFieldRel, + this.typeormUtilsService.getAliasForRelation(rel.toString()) + ) + ); + } + } + } + } + + if (include) { + for (const rel of include) { + const currentIncludeAlias = + this.typeormUtilsService.getAliasForRelation(rel); + + builder[fields ? 'leftJoin' : 'leftJoinAndSelect']( + this.typeormUtilsService.getAliasPath(rel), + currentIncludeAlias + ); + + if (fields) { + selectFields.add( + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.getPrimaryColumnForRel(rel), + currentIncludeAlias + ) + ); + } + } + } + if (selectFields.size > 0) { + builder.select([...selectFields]); + } + const paramsId = 'paramsId'; + const result = await builder + .where( + `${this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + )} = :${paramsId}`, + { + [paramsId]: id, + } + ) + .getOne(); + + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + const { included, data } = this.transformDataService.transformData(result); + return { + meta: {}, + data, + ...(included ? { included } : {}), + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts new file mode 100644 index 00000000..161d1f83 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts @@ -0,0 +1,131 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; + +import { NotFoundException } from '@nestjs/common'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('getRelationship', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Should be ok', async () => { + const spyOnTransformData = jest.spyOn( + transformDataService, + 'getRelationships' + ); + const id = 1; + const rel = 'roles'; + const check = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + }, + where: { id }, + relations: { + roles: true, + }, + }); + const result = await typeormService.getRelationship(id, rel); + expect(spyOnTransformData).toBeCalledWith(check, rel); + expect(result).toHaveProperty('data'); + }); + it('Should be error', async () => { + expect.assertions(1); + try { + await typeormService.getRelationship('1000000', 'roles'); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts new file mode 100644 index 00000000..d50f45a9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts @@ -0,0 +1,71 @@ +import { + EntityRelation, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; + +import { + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { TypeOrmService } from '../../service'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; + +export async function getRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: TypeOrmService, + id: number | string, + rel: Rel +): Promise> { + const paramsId = 'paramsId'; + const result = await this.repository + .createQueryBuilder() + .select([ + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + ), + this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.getPrimaryColumnForRel(rel.toString()), + this.typeormUtilsService.getAliasForRelation(rel.toString()) + ), + ]) + .where( + ` + ${this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + )} = :paramsId + ` + ) + .leftJoin( + this.typeormUtilsService.getAliasPath(rel.toString()), + this.typeormUtilsService.getAliasForRelation(rel.toString()) + ) + .setParameters({ + [paramsId]: id, + }) + .getOne(); + + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + + const { data } = this.transformDataService.getRelationships(result, rel); + if (data === undefined) { + const error: ValidateQueryError = { + code: 'custom', + message: `transformDataService.getRelationships return undefined`, + path: [], + }; + throw new InternalServerErrorException([error]); + } + return { + meta: {}, + data, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts new file mode 100644 index 00000000..4de29ce2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts @@ -0,0 +1,21 @@ +export { getAll } from './get-all/get-all'; +export { getOne } from './get-one/get-one'; +export { deleteOne } from './delete-one/delete-one'; +export { postOne } from './post-one/post-one'; +export { patchOne } from './patch-one/patch-one'; +export { getRelationship } from './get-relationship/get-relationship'; +export { postRelationship } from './post-relationship/post-relationship'; +export { deleteRelationship } from './delete-relationship/delete-relationship'; +export { patchRelationship } from './patch-relationship/patch-relationship'; + +// export const MethodsService = { +// getAll, +// getOne, +// deleteOne, +// postOne, +// patchOne, +// getRelationship, +// postRelationship, +// deleteRelationship, +// patchRelationship, +// }; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts new file mode 100644 index 00000000..9cc08057 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts @@ -0,0 +1,308 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IBackup, IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; + +import { PatchData, PostData } from '../../../mixin/zod'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('patchOne', () => { + let db: IMemoryDb; + let backaUp: IBackup; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + const firstName = 'firstName test'; + const isActive = false; + const testDate = new Date(); + const login = 'login test'; + + let inputData: PostData; + let newData: PatchData; + + let notes: Notes[]; + let users: Users[]; + let roles: Roles[]; + let userGroup: UserGroups[]; + let addresses: Addresses[]; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + + notes = await notesRepository.find(); + users = await userRepository.find(); + roles = await rolesRepository.find(); + userGroup = await userGroupRepository.find({ + relations: { + users: true, + }, + }); + addresses = await addressesRepository.find(); + + inputData = { + type: 'users', + attributes: { + firstName, + isActive, + testDate, + login, + }, + relationships: { + addresses: { + data: { + type: 'addresses', + id: addresses[0].id.toString(), + }, + }, + notes: { + data: [ + { + type: 'notes', + id: notes[0].id, + }, + ], + }, + roles: { + data: [ + { + type: 'roles', + id: `${roles[0].id}`, + }, + ], + }, + manager: { + data: { + type: 'users', + id: `${users[0].id}`, + }, + }, + userGroup: { + data: { + type: 'user-group', + id: `${userGroup[0].id}`, + }, + }, + }, + }; + + await typeormService.postOne(inputData); + backaUp = db.backup(); + const changeUser = await userRepository.findOneBy({ + login: inputData.attributes.login as string, + }); + if (!changeUser) { + throw new Error('not found mock data'); + } + newData = { + ...inputData, + id: `${changeUser.id}`, + }; + const newLogin = `${changeUser.login} - newLogin`; + const newIsActive = !changeUser.isActive; + + newData.attributes.login = newLogin; + newData.attributes.isActive = newIsActive; + newData.attributes.testDate = new Date(); + + newData.relationships = { + ...newData.relationships, + manager: { + data: { + type: 'users', + id: users[1].id.toString(), + }, + }, + addresses: { + data: null, + }, + userGroup: { + data: { + type: 'user-group', + id: `${userGroup[1].id}`, + }, + }, + roles: { + data: [ + { + type: 'roles', + id: `${roles[1].id}`, + }, + ], + }, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + backaUp.restore(); + }); + + it('should be ok without relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + })); + + const { relationships, ...withoutRelationships } = newData; + const returnData = await typeormService.patchOne( + withoutRelationships.id as string, + withoutRelationships + ); + + const result = await userRepository.findOneBy({ + id: parseInt(withoutRelationships.id as string, 10), + }); + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).not.toHaveProperty('included'); + }); + + it('should be ok with relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + included: {} as any, + })); + + const returnData = await typeormService.patchOne( + newData.id as string, + newData + ); + + const result = await userRepository.findOne({ + where: { + id: parseInt(newData.id as string, 10), + }, + relations: { + addresses: true, + notes: true, + userGroup: true, + roles: true, + manager: true, + }, + }); + + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).toHaveProperty('included'); + }); + + it('should be ok with relation nulling relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + included: {} as any, + })); + + newData.relationships = { + ...newData.relationships, + userGroup: { + data: null, + }, + roles: { + data: [], + }, + }; + + const returnData = await typeormService.patchOne( + newData.id as string, + newData + ); + + const result = await userRepository.findOne({ + where: { + id: parseInt(newData.id as string, 10), + }, + relations: { + addresses: true, + notes: true, + userGroup: true, + roles: true, + manager: true, + }, + }); + + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).toHaveProperty('included'); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts new file mode 100644 index 00000000..90b57273 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts @@ -0,0 +1,73 @@ +import { + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { DeepPartial } from 'typeorm'; +import { ResourceObject, ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { PatchData } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; + +export async function patchOne( + this: TypeOrmService, + id: number | string, + inputData: PatchData +): Promise> { + const { id: idBody, attributes, relationships } = inputData; + + if (`${id}` !== idBody) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Data 'id' must be equal to url param`, + path: ['data', 'id'], + }; + + throw new UnprocessableEntityException([error]); + } + + const paramsId = 'paramsId'; + const result = await this.repository + .createQueryBuilder() + .where( + `${this.typeormUtilsService.getAliasPath( + this.typeormUtilsService.currentPrimaryColumn + )} = :${paramsId}`, + { + [paramsId]: id, + } + ) + .getOne(); + + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.typeormUtilsService.currentAlias}' with id '${id}' does not exist`, + path: ['data', 'id'], + }; + throw new NotFoundException([error]); + } + + if (attributes) { + const entityTarget = this.repository.manager.create( + this.repository.target, + attributes as DeepPartial + ); + for (const [props, val] of ObjectTyped.entries(entityTarget)) { + result[props] = val; + } + } + + const saveData = await this.typeormUtilsService.saveEntityData( + result, + relationships + ); + + const { data, included } = this.transformDataService.transformData(saveData); + const includeData = included ? { included } : {}; + return { + meta: {}, + data, + ...includeData, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts new file mode 100644 index 00000000..2cfe5343 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts @@ -0,0 +1,230 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('patchRelationship', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + let typeormUtilsService: TypeormUtilsService; + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + typeormUtilsService = + module.get>(TypeormUtilsService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Should be ok', async () => { + const checkUser = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + + const roles = await rolesRepository.find(); + const userGroups = await userGroupRepository.find(); + const users = await userRepository.find(); + + if (!checkUser) { + throw new Error('not found mock'); + } + + const userGroupData = { + type: 'user-groups', + id: userGroups + .find((i) => checkUser.userGroup.id !== i.id) + ?.id.toString(), + }; + const rolesData = [ + { + type: 'roles', + id: roles + .find((i) => checkUser.roles.find((a) => a.id !== i.id)) + ?.id.toString(), + }, + ]; + + const managerData = { + type: 'users', + id: users.find((i) => checkUser.manager.id !== i.id)?.id.toString(), + }; + const result = await typeormService.patchRelationship( + 1, + 'roles', + rolesData as any + ); + const result1 = await typeormService.patchRelationship( + 1, + 'userGroup', + userGroupData as any + ); + const result2 = await typeormService.patchRelationship( + 1, + 'manager', + managerData as any + ); + + const checkUserAfterPost = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + if (!checkUserAfterPost) { + throw new Error('not found'); + } + expect(checkUserAfterPost.manager.id.toString()).toBe(managerData.id); + expect(checkUserAfterPost.roles.map((i) => i.id.toString())).toEqual( + rolesData.map((i) => i.id) + ); + expect(checkUserAfterPost.userGroup.id.toString()).toBe(userGroupData.id); + + await typeormService.patchRelationship(1, 'roles', []); + await typeormService.patchRelationship(1, 'manager', null); + const checkUserAfterPatch = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + if (!checkUserAfterPatch) { + throw new Error('not found'); + } + + expect(checkUserAfterPatch.manager).toBe(null); + expect(checkUserAfterPatch.roles).toEqual([]); + expect(result.data.map((i) => i.id)).toEqual( + checkUserAfterPost.roles.map((i) => i.id.toString()) + ); + expect(result2.data?.id).toEqual(checkUserAfterPost.manager.id.toString()); + expect(result1.data?.id).toEqual( + checkUserAfterPost.userGroup.id.toString() + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts new file mode 100644 index 00000000..c8ed4f81 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts @@ -0,0 +1,51 @@ +import { + EntityRelation, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; + +import { PatchRelationshipData } from '../../../mixin/zod'; +import { getRelationship } from '../get-relationship/get-relationship'; +import { TypeOrmService } from '../../service'; + +export async function patchRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: TypeOrmService, + id: number | string, + rel: Rel, + input: PatchRelationshipData +): Promise> { + const idsResult = await this.typeormUtilsService.validateRelationInputData( + rel, + input + ); + + const patchBuilder = this.repository + .createQueryBuilder() + .relation(rel.toString()) + .of(id); + + if (Array.isArray(idsResult)) { + const data = await getRelationship.call< + TypeOrmService, + [number | string, Rel], + Promise> + >(this, id, rel); + const idsToDelete = Array.isArray(data.data) + ? data.data.map((i) => i.id) + : []; + + await patchBuilder.addAndRemove(idsResult, idsToDelete); + } else { + await patchBuilder.set(idsResult); + } + + return getRelationship.call< + TypeOrmService, + [number | string, Rel], + Promise> + >(this, id, rel); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts new file mode 100644 index 00000000..a2e45206 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts @@ -0,0 +1,256 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IBackup, IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + Pods, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; + +import { PostData } from '../../../mixin/zod'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('postOne', () => { + let db: IMemoryDb; + let backaUp: IBackup; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + let podsRepository: Repository; + + let typeormServicePods: TypeOrmService; + let transformDataServicePods: TransformDataService; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + const firstName = 'firstName test'; + const isActive = false; + const testDate = new Date(); + const login = 'login test'; + + let inputData: PostData; + + let notes: Notes[]; + let users: Users[]; + let roles: Roles[]; + let userGroup: UserGroups[]; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + + const modulePods: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Pods), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + podsRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + backaUp = db.backup(); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + + typeormServicePods = modulePods.get>(ORM_SERVICE); + transformDataServicePods = + modulePods.get>(TransformDataService); + + notes = await notesRepository.find(); + users = await userRepository.find(); + roles = await rolesRepository.find(); + userGroup = await userGroupRepository.find(); + + inputData = { + type: 'users', + attributes: { + firstName, + isActive, + testDate, + login, + }, + relationships: { + notes: { + data: [ + { + type: 'notes', + id: notes[0].id, + }, + ], + }, + roles: { + data: [ + { + type: 'roles', + id: `${roles[0].id}`, + }, + ], + }, + manager: { + data: { + type: 'users', + id: `${users[0].id}`, + }, + }, + userGroup: { + data: { + type: 'user-group', + id: `${userGroup[0].id}`, + }, + }, + }, + }; + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + backaUp.restore(); + }); + + it('should be ok without relation and with id', async () => { + const spyOnTransformData = jest + .spyOn(transformDataServicePods, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + })); + const { relationships, ...other } = inputData; + const id = '5'; + const returnData = await typeormServicePods.postOne({ + id, + type: 'pods', + attributes: { + name: 'test', + }, + }); + const result = await podsRepository.findOneBy({ + id, + }); + + expect(spyOnTransformData).toBeCalledWith({ + ...result, + id, + }); + expect(returnData).not.toHaveProperty('included'); + }); + + it('should be ok without relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + })); + const { relationships, ...other } = inputData; + const returnData = await typeormService.postOne(other); + const result = await userRepository.findOneBy({ + login, + }); + + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).not.toHaveProperty('included'); + }); + + it('should be ok with relation', async () => { + const spyOnTransformData = jest + .spyOn(transformDataService, 'transformData') + .mockImplementationOnce(() => ({ + data: {} as any, + included: {} as any, + })); + const returnData = await typeormService.postOne(inputData); + const result = await userRepository.findOne({ + where: { + login, + }, + relations: { + notes: true, + userGroup: true, + roles: true, + manager: true, + }, + }); + + expect(spyOnTransformData).toBeCalledWith(result); + expect(returnData).toHaveProperty('included'); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts new file mode 100644 index 00000000..d4decd3b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts @@ -0,0 +1,39 @@ +import { DeepPartial } from 'typeorm'; +import { ResourceObject } from '@klerick/json-api-nestjs-shared'; +import { ObjectLiteral } from '../../../../types'; +import { PostData } from '../../../mixin/zod'; +import { TypeOrmService } from '../../service'; + +export async function postOne( + this: TypeOrmService, + inputData: PostData +): Promise> { + const { attributes, relationships, id } = inputData; + + const idObject = id + ? { [this.typeormUtilsService.currentPrimaryColumn.toString()]: id } + : {}; + + const attributesObject = { + ...attributes, + ...idObject, + } as DeepPartial; + + const entityTarget = this.repository.manager.create( + this.repository.target, + attributesObject + ); + + const saveData = await this.typeormUtilsService.saveEntityData( + entityTarget, + relationships + ); + + const { data, included } = this.transformDataService.transformData(saveData); + const includeData = included ? { included } : {}; + return { + meta: {}, + data, + ...includeData, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts new file mode 100644 index 00000000..a1126f32 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts @@ -0,0 +1,205 @@ +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils'; + +import { + CONTROL_OPTIONS_TOKEN, + DEFAULT_CONNECTION_NAME, + ORM_SERVICE, +} from '../../../../constants'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, + OrmServiceFactory, +} from '../../factory'; +import { + EntityPropsMapService, + TypeOrmService, + TransformDataService, + TypeormUtilsService, +} from '../../service'; + +describe('postRelationship', () => { + let db: IMemoryDb; + let typeormService: TypeOrmService; + let transformDataService: TransformDataService; + let typeormUtilsService: TypeormUtilsService; + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CONTROL_OPTIONS_TOKEN, + useValue: { + requiredSelectField: false, + debug: false, + }, + }, + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + TransformDataService, + OrmServiceFactory(), + EntityPropsMapService, + ], + }).compile(); + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + typeormService = module.get>(ORM_SERVICE); + transformDataService = + module.get>(TransformDataService); + typeormUtilsService = + module.get>(TypeormUtilsService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Should be ok', async () => { + const checkUser = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + + const roles = await rolesRepository.find(); + const userGroups = await userGroupRepository.find(); + const users = await userRepository.find(); + + if (!checkUser) { + throw new Error('not found mock'); + } + + const userGroupData = { + type: 'user-groups', + id: userGroups + .find((i) => checkUser.userGroup.id !== i.id) + ?.id.toString(), + }; + const rolesData = [ + { + type: 'roles', + id: roles + .find((i) => checkUser.roles.find((a) => a.id !== i.id)) + ?.id.toString(), + }, + ]; + + const managerData = { + type: 'users', + id: users.find((i) => checkUser.manager.id !== i.id)?.id.toString(), + }; + const result = await typeormService.postRelationship( + 1, + 'roles', + rolesData as any + ); + const result1 = await typeormService.postRelationship( + 1, + 'userGroup', + userGroupData as any + ); + + const result2 = await typeormService.postRelationship( + 1, + 'manager', + managerData as any + ); + + const checkUserAfterPost = await userRepository.findOne({ + select: { + id: true, + roles: { + id: true, + }, + userGroup: { + id: true, + }, + manager: { + id: true, + }, + }, + where: { id: 1 }, + relations: { + roles: true, + manager: true, + userGroup: true, + }, + }); + if (!checkUserAfterPost) { + throw new Error('not found'); + } + + expect(checkUserAfterPost.manager.id.toString()).toBe(managerData.id); + expect(checkUserAfterPost.roles.map((i) => i.id.toString())).toEqual([ + ...checkUser.roles.map((i) => i.id.toString()), + ...rolesData.map((i) => i.id), + ]); + expect(checkUserAfterPost.userGroup.id.toString()).toBe(userGroupData.id); + + expect(result.data.map((i) => i.id)).toEqual( + checkUserAfterPost.roles.map((i) => i.id.toString()) + ); + expect(result2.data?.id).toEqual(checkUserAfterPost.manager.id.toString()); + expect(result1.data?.id).toEqual( + checkUserAfterPost.userGroup.id.toString() + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts new file mode 100644 index 00000000..695c9ff1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts @@ -0,0 +1,40 @@ +import { + EntityRelation, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; +import { PostRelationshipData } from '../../../mixin/zod'; +import { getRelationship } from '../get-relationship/get-relationship'; +import { TypeOrmService } from '../../service'; + +export async function postRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: TypeOrmService, + id: number | string, + rel: Rel, + input: PostRelationshipData +): Promise> { + const idsResult = await this.typeormUtilsService.validateRelationInputData( + rel, + input + ); + const postBuilder = this.repository + .createQueryBuilder() + .relation(rel.toString()) + .of(id); + + if (Array.isArray(idsResult)) { + await postBuilder.add(idsResult); + } else { + await postBuilder.set(idsResult); + } + + return getRelationship.call< + TypeOrmService, + [number | string, Rel], + Promise> + >(this, id, rel); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts new file mode 100644 index 00000000..981d2db4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts @@ -0,0 +1,85 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; + +import { + createAndPullSchemaBase, + mockDBTestModule, + providerEntities, + UserGroups, + Users, +} from '../../../mock-utils'; +import { CurrentDataSourceProvider } from '../factory'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; +import { EntityPropsMapService } from './entity-props-map.service'; + +describe('EntityPropsMapService', () => { + let db: IMemoryDb; + let entityPropsMapService: EntityPropsMapService; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + EntityPropsMapService, + ], + }).compile(); + + entityPropsMapService = module.get( + EntityPropsMapService + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('getPropsForEntity', () => { + const result = entityPropsMapService.getRelPropsForEntity(Users); + expect(result).toEqual([ + 'addresses', + 'manager', + 'roles', + 'comments', + 'notes', + 'userGroup', + ]); + }); + + it('getPropsForEntity', () => { + const result = entityPropsMapService.getPropsForEntity(Users); + expect(result).toEqual([ + 'id', + 'login', + 'firstName', + 'testReal', + 'testArrayNull', + 'lastName', + 'isActive', + 'createdAt', + 'testDate', + 'updatedAt', + ]); + }); + + it('getPrimaryColumnsForEntity', () => { + expect(entityPropsMapService.getPrimaryColumnsForEntity(Users)).toBe('id'); + }); + + it('getNameForEntity', () => { + expect(entityPropsMapService.getNameForEntity(Users)).toBe('Users'); + expect(entityPropsMapService.getNameForEntity(UserGroups)).toBe( + 'UserGroups' + ); + }); + + it('getRelationPropsType', () => { + expect( + entityPropsMapService.getRelationPropsType(Users, 'userGroup' as any) + ).toEqual(UserGroups); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts new file mode 100644 index 00000000..160f2028 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts @@ -0,0 +1,102 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, EntityTarget } from 'typeorm'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +import { CURRENT_DATA_SOURCE_TOKEN } from '../constants'; +import { ObjectLiteral as Entity } from '../../../types'; +import { + ResultGetField, + TupleOfEntityProps, + TupleOfEntityRelation, +} from '../../mixin/types'; +import { getField } from '../orm-helper'; + +@Injectable() +export class EntityPropsMapService { + @Inject(CURRENT_DATA_SOURCE_TOKEN) private dataSource!: DataSource; + + private _propsForEntity: Map = new Map(); + private _relPropsForEntity: Map = new Map(); + private _relTypePropsForEntity: Map = new Map(); + private _primaryColumnsForEntity: Map = new Map(); + private _nameForEntity: Map = new Map(); + + getPropsForEntity(entity: E): TupleOfEntityProps { + const result = this._propsForEntity.get(entity); + if (result) { + return result; + } + + const { field } = this.pullPropsAndRelFoEntity(entity); + + return field; + } + + getRelPropsForEntity(entity: E): TupleOfEntityRelation { + const result = this._relPropsForEntity.get(entity); + if (result) { + return result; + } + + const { relations } = this.pullPropsAndRelFoEntity(entity); + + return relations; + } + + getRelationPropsType(entity: E, rel: EntityRelation) { + const result = this._relTypePropsForEntity.get(entity); + if (result) { + return result[rel]; + } + + const repo = this.dataSource.getRepository( + entity as unknown as EntityTarget + ); + const relToType = repo.metadata.relations.reduce((acum, item) => { + acum[item.propertyName as keyof E] = item.inverseEntityMetadata.target; + return acum; + }, {} as Record); + + this._relTypePropsForEntity.set(entity, relToType); + return relToType[rel]; + } + + getPrimaryColumnsForEntity(entity: E): keyof E { + const result = this._primaryColumnsForEntity.get(entity); + if (result) { + return result as keyof E; + } + const primaryColumns = this.dataSource.getRepository( + entity as unknown as EntityTarget + ).metadata.primaryColumns[0].propertyName; + this._primaryColumnsForEntity.set(entity, primaryColumns); + + return primaryColumns as keyof E; + } + + getNameForEntity(entity: E): string { + const result = this._nameForEntity.get(entity); + if (result) { + return result; + } + const name = this.dataSource.getRepository( + entity as unknown as EntityTarget + ).metadata.name; + this._nameForEntity.set(entity, name); + return name; + } + + private pullPropsAndRelFoEntity( + entity: E + ): ResultGetField { + const repo = this.dataSource.getRepository( + entity as unknown as EntityTarget + ); + + const { relations, field } = getField(repo); + this._propsForEntity.set(entity, field); + this._relPropsForEntity.set(entity, relations); + + return { relations, field }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts new file mode 100644 index 00000000..2a3590c8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts @@ -0,0 +1,4 @@ +export * from './type-orm.service'; +export * from './transform-data.service'; +export * from './typeorm-utils.service'; +export * from './entity-props-map.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts new file mode 100644 index 00000000..fed6e89a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts @@ -0,0 +1,372 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { + Addresses, + Comments, + createAndPullSchemaBase, + getRepository, + mockDBTestModule, + Notes, + providerEntities, + pullAllData, + Roles, + UserGroups, + Users, +} from '../../../mock-utils'; +import { CurrentDataSourceProvider, CurrentEntityRepository } from '../factory'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; +import { TransformDataService } from './transform-data.service'; +import { ApplicationConfig } from '@nestjs/core'; +import { VersioningType } from '@nestjs/common'; +import { EntityPropsMapService } from '../service'; + +describe('TransformDataService', () => { + let db: IMemoryDb; + let transformDataService: TransformDataService; + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + let applicationConfig: ApplicationConfig; + let entityPropsMapService: EntityPropsMapService; + let usersData: Users; + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + // TransformDataService, + // EntityPropsMapService, + ], + }).compile(); + + // ({ + // userRepository, + // addressesRepository, + // notesRepository, + // commentsRepository, + // rolesRepository, + // userGroupRepository, + // } = getRepository(module)); + // await pullAllData( + // userRepository, + // addressesRepository, + // notesRepository, + // commentsRepository, + // rolesRepository, + // userGroupRepository + // ); + + // transformDataService = + // module.get>(TransformDataService); + // entityPropsMapService = module.get( + // EntityPropsMapService + // ); + // + // const data = await userRepository.findOne({ + // where: { + // id: 1, + // }, + // relations: { + // userGroup: true, + // manager: true, + // roles: true, + // addresses: true, + // comments: true, + // notes: true, + // }, + // }); + // if (!data) { + // throw new Error('Not found user'); + // } + // usersData = data; + // applicationConfig = module.get(ApplicationConfig); + }); + + beforeEach(() => { + // transformDataService = new TransformDataService(); + // Object.defineProperty(transformDataService, 'applicationConfig', { + // value: applicationConfig, + // }); + // Object.defineProperty(transformDataService, 'entityPropsMapService', { + // value: entityPropsMapService, + // }); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('Test', () => {}); + + // it('Url path', () => { + // const prefix = 'api'; + // applicationConfig.setGlobalPrefix(prefix); + // + // expect(transformDataService.urlPath).toEqual(['', prefix]); + // }); + // it('Url path with version', () => { + // const prefix = 'api'; + // const version = '1'; + // + // applicationConfig.setGlobalPrefix(prefix); + // + // jest + // .spyOn(applicationConfig, 'getVersioning') + // .mockImplementationOnce(() => ({ + // type: VersioningType.URI, + // defaultVersion: version, + // })); + // + // expect(transformDataService.urlPath).toEqual(['', prefix, 'v1']); + // }); + // + // it('check type, id and links', () => { + // const result = transformDataService.transformData(usersData); + // expect(result).toHaveProperty('data'); + // const { data } = result; + // expect(data.type).toBe('users'); + // expect(data.id).toBe(usersData.id.toString()); + // expect(data.links.self).toBe( + // [...transformDataService.urlPath, 'users', usersData.id.toString()].join( + // '/' + // ) + // ); + // }); + // + // it('check attributes', async () => { + // const result = transformDataService.transformData(usersData); + // const { + // id, + // addresses, + // comments, + // manager, + // roles, + // notes, + // userGroup, + // ...other + // } = usersData; + // expect(result.data.attributes).toEqual(other); + // const result1 = transformDataService.transformData([]); + // expect(result1.data).toEqual([]); + // + // const data = await userRepository.findOne({ + // select: { + // id: true, + // login: true, + // isActive: true, + // testDate: true, + // }, + // where: { + // id: 1, + // }, + // }); + // if (!data) { + // throw new Error('Not found user'); + // } + // const result2 = transformDataService.transformData(data); + // const { id: id2, ...other2 } = data; + // + // expect(result2.data.attributes).toEqual(other2); + // }); + // + // it('check relationships', async () => { + // const { + // data: { relationships }, + // } = transformDataService.transformData(usersData); + // expect(relationships?.addresses?.data).toEqual({ + // type: 'addresses', + // id: usersData.addresses.id.toString(), + // }); + // expect(relationships?.addresses?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/addresses` + // ); + // expect(relationships?.manager?.data).toEqual({ + // type: 'users', + // id: usersData.manager.id.toString(), + // }); + // expect(relationships?.manager?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/manager` + // ); + // expect(relationships?.roles?.data).toEqual( + // usersData.roles.map((i) => ({ + // type: 'roles', + // id: i.id.toString(), + // })) + // ); + // expect(relationships?.roles?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/roles` + // ); + // expect(relationships?.comments?.data).toEqual( + // usersData.comments.map((i) => ({ + // type: 'comments', + // id: i.id.toString(), + // })) + // ); + // expect(relationships?.comments?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/comments` + // ); + // expect(relationships?.notes?.data).toEqual( + // usersData.notes.map((i) => ({ + // type: 'notes', + // id: i.id.toString(), + // })) + // ); + // expect(relationships?.notes?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/notes` + // ); + // expect(relationships?.userGroup?.data).toEqual({ + // type: 'user-groups', + // id: usersData.userGroup.id.toString(), + // }); + // expect(relationships?.userGroup?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/userGroup` + // ); + // }); + // + // it('check relationships again', async () => { + // const data = await userRepository.findOne({ + // where: { + // id: 1, + // }, + // relations: { + // userGroup: true, + // roles: true, + // }, + // }); + // if (!data) { + // throw new Error('Not found user'); + // } + // + // const { + // data: { relationships }, + // } = transformDataService.transformData(data); + // + // expect(relationships?.addresses).not.toHaveProperty('data'); + // expect(relationships?.addresses?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/addresses` + // ); + // expect(relationships?.manager).not.toHaveProperty('data'); + // expect(relationships?.manager?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/manager` + // ); + // expect(relationships?.roles?.data).toEqual( + // usersData.roles.map((i) => ({ + // type: 'roles', + // id: i.id.toString(), + // })) + // ); + // expect(relationships?.roles?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/roles` + // ); + // expect(relationships?.comments).not.toHaveProperty('data'); + // expect(relationships?.comments?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/comments` + // ); + // expect(relationships?.notes).not.toHaveProperty('data'); + // expect(relationships?.notes?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/notes` + // ); + // expect(relationships?.userGroup?.data).toEqual({ + // type: 'user-groups', + // id: usersData.userGroup.id.toString(), + // }); + // expect(relationships?.userGroup?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/userGroup` + // ); + // data.userGroup = null as any; + // data.roles = []; + // const { + // data: { relationships: relationships2 }, + // } = transformDataService.transformData(data); + // + // expect(relationships2?.addresses).not.toHaveProperty('data'); + // expect(relationships2?.addresses?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/addresses` + // ); + // expect(relationships2?.manager).not.toHaveProperty('data'); + // expect(relationships2?.manager?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/manager` + // ); + // + // expect(relationships2?.roles?.data).toEqual([]); + // expect(relationships2?.roles?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/roles` + // ); + // expect(relationships2?.comments).not.toHaveProperty('data'); + // expect(relationships2?.comments?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/comments` + // ); + // expect(relationships2?.notes).not.toHaveProperty('data'); + // expect(relationships2?.notes?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/notes` + // ); + // expect(relationships2?.userGroup?.data).toEqual(null); + // expect(relationships2?.userGroup?.links.self).toBe( + // `${transformDataService.urlPath.join('/')}/users/${ + // usersData.id + // }/relationships/userGroup` + // ); + // }); + // + // it('check include', async () => { + // const data = await userRepository.findOne({ + // where: { + // id: 1, + // }, + // relations: { + // userGroup: true, + // roles: true, + // }, + // }); + // if (!data) { + // throw new Error('Not found user'); + // } + // + // const { included } = transformDataService.transformData(data); + // expect(included).not.toBe(undefined); + // }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts new file mode 100644 index 00000000..b6ac2c93 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts @@ -0,0 +1,259 @@ +import { Inject, Injectable, VersioningType } from '@nestjs/common'; +import { ApplicationConfig } from '@nestjs/core'; +import { + Attributes, + Data, + MainData, + Relationships, + ResourceData, + ResourceObject, + camelToKebab, + ObjectTyped, + EntityRelation, +} from '@klerick/json-api-nestjs-shared'; + +import { RoutePathFactory } from '@nestjs/core/router/route-path-factory'; +import { EntityPropsMapService } from './entity-props-map.service'; +import { ObjectLiteral } from '../../../types'; + +Injectable(); +export class TransformDataService { + @Inject(ApplicationConfig) private applicationConfig!: ApplicationConfig; + @Inject(EntityPropsMapService) + private entityPropsMapService!: EntityPropsMapService; + + private _urlPath!: string[]; + + get urlPath() { + if (this._urlPath) return [...this._urlPath]; + this._urlPath = ['']; + const prefix = this.applicationConfig.getGlobalPrefix(); + const version = this.applicationConfig.getVersioning(); + + const routePathFactory = new RoutePathFactory(this.applicationConfig); + + if (prefix) { + this._urlPath.push(this.applicationConfig.getGlobalPrefix()); + } + if (version && version.type === VersioningType.URI) { + const firstVersion = Array.isArray(version.defaultVersion) + ? version.defaultVersion[0] + : version.defaultVersion; + if (firstVersion) { + this._urlPath.push( + `${routePathFactory.getVersionPrefix( + version + )}${firstVersion.toString()}` + ); + } + } + return [...this._urlPath]; + } + private getLink(...partOfUrl: string[]) { + const urlPath = this.urlPath; + urlPath.push(...partOfUrl); + return urlPath.join('/'); + } + + public transformData(data: E): Pick, 'data' | 'included'>; + public transformData( + data: E[] + ): Pick, 'data' | 'included'>; + public transformData( + data: E | E[] + ): + | Pick, 'data' | 'included'> + | Pick, 'data' | 'included'> { + const dataResponse = Array.isArray(data) + ? data + .map((i) => this.getMainData(i)) + .filter((i: ResourceData | null): i is ResourceData => !!i) + : this.getMainData(data); + + let relationships: Relationships | undefined = undefined; + if (Array.isArray(dataResponse) && dataResponse.length > 0) { + relationships = dataResponse.reduce( + (acum, item) => ({ + ...acum, + ...item.relationships, + }), + {} as Relationships + ); + } + + if (!Array.isArray(dataResponse) && dataResponse !== null) { + relationships = dataResponse.relationships; + } + + if (relationships === undefined) { + return { data: dataResponse } as Pick< + ResourceObject, + 'data' | 'included' + >; + } + + const propsNeedForInclude = ObjectTyped.entries(relationships).reduce( + (acum, [key, val]) => { + if (!val || !('data' in val)) { + return acum; + } + if (Array.isArray(val.data)) { + acum.push(key); + return acum; + } + if (!Array.isArray(val.data)) { + acum.push(key); + } + return acum; + }, + [] as EntityRelation[] + ); + + const included = (Array.isArray(data) ? data : [data]) + .reduce((acum, item) => { + const tmp = propsNeedForInclude.reduce((a, props) => { + const currentItem = item[props]; + type CurrentItem = typeof currentItem; + const r = ( + Array.isArray(currentItem) ? currentItem : [currentItem] + ) as CurrentItem[]; + + a.push(...r); + return a; + }, [] as E[EntityRelation][]); + acum.push(...tmp); + return acum; + }, [] as E[EntityRelation][]) + .map((i) => { + return this.getMainData(i as E); + }) + .filter((i) => !!i); + + if (included.length > 0) { + return { data: dataResponse, included } as unknown as Pick< + ResourceObject, + 'data' | 'included' + >; + } + return { data: dataResponse } as unknown as Pick< + ResourceObject, + 'data' | 'included' + >; + } + + getMainData(data: E): ResourceData | null { + if (!data) return null; + const entity = data.constructor as unknown as E; + return { + ...this.getData( + camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), + data[this.entityPropsMapService.getPrimaryColumnsForEntity(entity)] as + | string + | number + ), + attributes: ObjectTyped.entries(data).reduce((acum, [key, val]) => { + if ( + this.entityPropsMapService + .getRelPropsForEntity(entity) + .includes(key.toString()) || + key.toString() === + this.entityPropsMapService.getPrimaryColumnsForEntity(entity) + ) { + return acum; + } + acum[key] = val; + return acum; + }, {} as Record) as Attributes, + relationships: this.transformRelationshipsData(data), + links: { + self: this.getLink( + camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), + data[ + this.entityPropsMapService.getPrimaryColumnsForEntity(entity) + ] as string + ), + }, + }; + } + + getRelationships>( + data: E, + rel: Rel + ): Data { + const entity = data.constructor as unknown as E; + const relation = data[rel]; + const target = this.entityPropsMapService.getRelationPropsType(entity, rel); + const typeName = camelToKebab( + this.entityPropsMapService.getNameForEntity(target) + ) as Rel; + + const primaryColumn = + this.entityPropsMapService.getPrimaryColumnsForEntity(target); + + const resultData = Array.isArray(relation) + ? (relation as E[Rel][]).map((i: E[Rel]) => + this.getData( + typeName, + i[primaryColumn as keyof E[Rel]] as string + ) + ) + : relation === null || !relation + ? null + : this.getData( + typeName, + relation[primaryColumn as keyof E[Rel]] as string + ); + + return { + data: resultData, + } as Data; + } + + transformRelationshipsData(data: E): Relationships { + const entity = data.constructor as unknown as E; + return this.entityPropsMapService + .getRelPropsForEntity(entity) + .reduce((acum: any, val: any) => { + const result: { + links: Record; + data?: Data], EntityRelation>['data']; + } = { + links: { + self: this.getLink( + camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), + data[ + this.entityPropsMapService.getPrimaryColumnsForEntity( + entity + ) as keyof E + ] as string, + 'relationships', + val + ), + }, + }; + + if (val in data) { + const { data: dataRelationships } = this.getRelationships( + data, + val as EntityRelation + ); + if (Array.isArray(dataRelationships)) { + result['data'] = dataRelationships; + } + + if (!Array.isArray(dataRelationships)) { + result['data'] = dataRelationships; + } + } + acum[val.toString()] = result; + return acum; + }, {} as Record) as Relationships; + } + + getData(type: T, id: number | string): MainData { + return { + type, + id: id.toString(), + }; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts new file mode 100644 index 00000000..2e80c4f2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts @@ -0,0 +1,135 @@ +import { + ResourceObject, + EntityRelation, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; +import { Inject } from '@nestjs/common'; +import { Repository } from 'typeorm'; + +import { OrmService } from '../../mixin/types'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../../mixin/zod'; +import { ConfigParam, ObjectLiteral } from '../../../types'; + +import { + getAll, + getOne, + deleteOne, + postOne, + patchOne, + getRelationship, + postRelationship, + deleteRelationship, + patchRelationship, +} from '../orm-methods'; + +import { TypeormUtilsService } from './typeorm-utils.service'; +import { TransformDataService } from './transform-data.service'; +import { CONTROL_OPTIONS_TOKEN } from '../../../constants'; +import { CURRENT_ENTITY_REPOSITORY } from '../constants'; +import { TypeOrmParam } from '../type'; + +export class TypeOrmService implements OrmService { + @Inject(TypeormUtilsService) + public typeormUtilsService!: TypeormUtilsService; + @Inject(TransformDataService) + public transformDataService!: TransformDataService; + @Inject(CONTROL_OPTIONS_TOKEN) public config!: ConfigParam & TypeOrmParam; + @Inject(CURRENT_ENTITY_REPOSITORY) public repository!: Repository; + + getAll(query: Query): Promise> { + return getAll.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, query); + } + + deleteOne(id: number | string): Promise { + return deleteOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id); + } + + deleteRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise { + return deleteRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } + + getOne(id: number | string, query: QueryOne): Promise> { + return getOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, query); + } + + getRelationship>( + id: number | string, + rel: Rel + ): Promise> { + return getRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel); + } + + patchOne( + id: number | string, + inputData: PatchData + ): Promise> { + return patchOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, inputData); + } + + patchRelationship>( + id: number | string, + rel: Rel, + input: PatchRelationshipData + ): Promise> { + return patchRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } + + postOne(inputData: PostData): Promise> { + return postOne.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, inputData); + } + + postRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise> { + return postRelationship.call< + TypeOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts new file mode 100644 index 00000000..9c32607f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts @@ -0,0 +1,770 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { IMemoryDb } from 'pg-mem'; +import { getDataSourceToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { + createAndPullSchemaBase, + mockDBTestModule, + providerEntities, + UserGroups, + Users, + Comments, + Roles, + Addresses, + Notes, + getRepository, + pullAllData, +} from '../../../mock-utils'; +import { + CurrentDataSourceProvider, + CurrentEntityManager, + CurrentEntityRepository, +} from '../factory'; +import { CURRENT_ENTITY_REPOSITORY } from '../constants'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; +import { TypeormUtilsService } from './typeorm-utils.service'; +import { PostData, PostRelationshipData, Query } from '../../mixin/zod'; +import { QueryField, FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { + EXPRESSION, + OperandsMapExpression, + ObjectLiteral as Entity, +} from '../../../types'; +import { + BadRequestException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; + +function getDefaultQuery() { + const filter = { + relation: null, + target: null, + }; + const defaultQuery: Query = { + [QueryField.filter]: filter, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: 1, + number: 1, + }, + }; + + return defaultQuery; +} + +describe('TypeormUtilsService', () => { + let db: IMemoryDb; + let typeormUtilsServiceUserGroups: TypeormUtilsService; + let repositoryUserGroups: Repository; + + let typeormUtilsServiceUser: TypeormUtilsService; + let repositoryUser: Repository; + + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + + function getQuery() { + return repositoryUser + .createQueryBuilder() + .subQuery() + .select('Users-Roles.user_id') + .from('users_have_roles', 'Users-Roles') + .leftJoin( + Roles, + 'Users__Roles_roles', + 'Users-Roles.role_id = Users__Roles_roles.id' + ); + } + + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + CurrentEntityManager(), + CurrentEntityRepository(UserGroups), + TypeormUtilsService, + ], + }).compile(); + + typeormUtilsServiceUserGroups = + module.get>(TypeormUtilsService); + repositoryUserGroups = module.get>( + CURRENT_ENTITY_REPOSITORY + ); + + const moduleUsers: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + ...providerEntities(getDataSourceToken()), + CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + CurrentEntityManager(), + CurrentEntityRepository(Users), + TypeormUtilsService, + ], + }).compile(); + + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + await pullAllData( + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository + ); + + typeormUtilsServiceUser = + moduleUsers.get>(TypeormUtilsService); + repositoryUser = moduleUsers.get>( + CURRENT_ENTITY_REPOSITORY + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('TypeormUtilsService.currentAlias', () => { + expect(typeormUtilsServiceUserGroups.currentAlias).toBe('UserGroups'); + }); + + it('TypeormUtilsService.getAliasForRelation', () => { + expect(typeormUtilsServiceUserGroups.getAliasForRelation('users')).toBe( + 'UserGroups__Users_users' + ); + }); + + it('TypeormUtilsService.getAliasPath', () => { + expect(typeormUtilsServiceUserGroups.getAliasPath('id')).toBe( + 'UserGroups.id' + ); + expect( + typeormUtilsServiceUserGroups.getAliasPath('Users', 'UserGroups') + ).toBe('UserGroups.Users'); + expect( + typeormUtilsServiceUserGroups.getAliasPath('Users', 'UserGroups', '-') + ).toBe('UserGroups-Users'); + expect( + typeormUtilsServiceUserGroups.getAliasPath('label', 'users', '-') + ).toBe('Users-label'); + }); + + describe('asyncIterateFindRelationships', () => { + it('should be ok', async () => { + const notes = await notesRepository.find(); + const userGroup = await userGroupRepository.find(); + + const data: PostData['relationships'] = { + notes: { + data: [ + { + type: 'notes', + id: notes[0].id, + }, + ], + }, + manager: { + data: { + type: 'users', + id: '1', + }, + }, + userGroup: { + data: { + type: 'users-group', + id: `${userGroup[0].id}`, + }, + }, + }; + + const result = []; + for await (const item of typeormUtilsServiceUser.asyncIterateFindRelationships( + data + )) { + result.push(item); + } + + expect(result[0]).toHaveProperty('notes'); + expect(result[0]['notes']).toEqual([{ id: notes[0].id }]); + + expect(result[1]).toHaveProperty('manager'); + expect(result[1]['manager']).toEqual({ id: 1 }); + + expect(result[2]).toHaveProperty('userGroup'); + expect(result[2]['userGroup']).toEqual({ id: userGroup[0].id }); + }); + + it('should be error props incorrect', async () => { + const data = { + incorrectProps: { + type: 'users', + id: '1', + }, + } as any; + expect.assertions(1); + try { + await typeormUtilsServiceUser + .asyncIterateFindRelationships(data) + .next(); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + + it('should be error resource not found', async () => { + const data: PostData['relationships'] = { + manager: { + data: { + id: '1000', + type: 'users', + }, + }, + }; + expect.assertions(1); + try { + await typeormUtilsServiceUser + .asyncIterateFindRelationships(data) + .next(); + } catch (e) { + expect(e).toBeInstanceOf(BadRequestException); + } + }); + }); + + describe('getFilterExpressionForTarget', () => { + it('expression for target field with null', () => { + const nullableField = 'id'; + const notNullableField = 'login'; + const query = getDefaultQuery(); + query.filter.target = { + [nullableField]: { + [FilterOperand.eq]: null, + }, + [notNullableField]: { + [FilterOperand.ne]: null, + }, + }; + + function guardField( + filter: any, + field: any + ): asserts field is keyof R { + if (filter && !(field in filter)) + throw new Error('field not exist in query filter'); + } + + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + const mainAliasCheck = 'Users'; + + for (const item of result) { + const { params, alias, expression, selectInclude } = item; + expect(selectInclude).toBe(undefined); + if (!alias) { + expect(alias).not.toBe(undefined); + throw new Error('alias in undefined for result'); + } + const [mainAlias, field] = alias.split('.'); + expect(mainAlias).toBe(mainAliasCheck); + guardField(query.filter.target, field); + const filterName: any = query.filter.target[field]; + if (!filterName) { + expect(filterName).not.toBe(undefined); + throw new Error('filterName in undefined from query'); + } + + expect(params).toBe(undefined); + + if (field === nullableField) { + expect(expression).toBe('IS NULL'); + continue; + } + + if (field === notNullableField) { + expect(expression).toBe('IS NOT NULL'); + continue; + } + + throw new Error('filed is incorrect'); + } + }); + it('expression for target field', () => { + const valueTest = (filterOperand: FilterOperand) => + `test for ${filterOperand}`; + const valueTestArray = ( + filterOperand: FilterOperand.nin | FilterOperand.in + ): [string, ...string[]] => [valueTest(filterOperand)]; + + const query = getDefaultQuery(); + query.filter.target = { + id: { + [FilterOperand.eq]: valueTest(FilterOperand.eq), + [FilterOperand.ne]: valueTest(FilterOperand.ne), + }, + isActive: { + [FilterOperand.like]: valueTest(FilterOperand.like), + [FilterOperand.regexp]: valueTest(FilterOperand.regexp), + }, + firstName: { + [FilterOperand.gt]: valueTest(FilterOperand.gt), + [FilterOperand.gte]: valueTest(FilterOperand.gte), + }, + testDate: { + [FilterOperand.lt]: valueTest(FilterOperand.lt), + [FilterOperand.lte]: valueTest(FilterOperand.lt), + }, + createdAt: { + [FilterOperand.in]: valueTestArray(FilterOperand.in), + [FilterOperand.nin]: valueTestArray(FilterOperand.nin), + }, + }; + + function guardField( + filter: any, + field: any + ): asserts field is keyof R { + if (filter && !(field in filter)) + throw new Error('field not exist in query filter'); + } + + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + const mainAliasCheck = 'Users'; + const paramsNameSet = new Set(); + for (const item of result) { + const { params, alias, expression, selectInclude } = item; + expect(selectInclude).toBe(undefined); + if (!alias) { + expect(alias).not.toBe(undefined); + throw new Error('alias in undefined for result'); + } + const [mainAlias, field] = alias.split('.'); + expect(mainAlias).toBe(mainAliasCheck); + guardField(query.filter.target, field); + const filterName: any = query.filter.target[field]; + if (!filterName) { + expect(filterName).not.toBe(undefined); + throw new Error('filterName in undefined from query'); + } + if (!params) { + expect(params).not.toBe(undefined); + throw new Error('params in undefined for result'); + } + if (Array.isArray(params)) { + expect(params).not.toBeInstanceOf(Array); + throw new Error('params in undefined for result'); + } + const { val, name } = params; + expect(paramsNameSet.has(name)).toBe(false); + paramsNameSet.add(name); + const reg = new RegExp(`params_${alias}_\\d{1,}`); + const regResult = name.match(reg); + + if (regResult === null) { + expect(name.match(reg)).not.toBe(null); + throw new Error(`name is not pattern: params_${alias}_\\d{1,}`); + } + const expressionMap = expression.replace(name, EXPRESSION); + const checkFilterOperand = Object.entries(FilterOperand).find( + ([key, val]) => OperandsMapExpression[val] === expressionMap + ); + if (!checkFilterOperand) { + expect(checkFilterOperand).not.toBe(undefined); + throw new Error(`expression incorrect`); + } + + const operand = checkFilterOperand[0] as any; + guardField(filterName, operand); + if (operand === 'like') { + expect(params.val).toEqual(`%${filterName[operand]}%`); + } else { + expect(params.val).toEqual(filterName[operand]); + } + } + }); + it('expression for target relation field with relation column', () => { + const query = getDefaultQuery(); + query.filter.target = { + addresses: { + [FilterOperand.eq]: 'null', + [FilterOperand.ne]: 'null', + }, + }; + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + expect(result.length).toBe(2); + const [first, second] = result; + expect(first).not.toHaveProperty('params'); + expect(first).not.toHaveProperty('selectInclude'); + expect(first['alias']).toBe('Users.addresses'); + expect(first['expression']).toBe('IS NULL'); + expect(second).not.toHaveProperty('params'); + expect(second).not.toHaveProperty('selectInclude'); + expect(second['alias']).toBe('Users.addresses'); + expect(second['expression']).toBe('IS NOT NULL'); + }); + it('expression for target relation field with one-to-many', () => { + const query = getDefaultQuery(); + query.filter.target = { + comments: { + [FilterOperand.eq]: 'null', + [FilterOperand.ne]: 'null', + }, + }; + const subQuery = repositoryUser + .createQueryBuilder() + .subQuery() + .select('Comments.createdBy', 'createdBy') + .from(Comments, 'Comments') + .where(`Comments.createdBy = Users.id`) + .getQuery(); + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + expect(result.length).toBe(2); + const [first, second] = result; + expect(first).not.toHaveProperty('params'); + expect(first).not.toHaveProperty('selectInclude'); + expect(first).not.toHaveProperty('alias'); + expect(first['expression']).toBe(`NOT EXISTS ${subQuery}`); + expect(second).not.toHaveProperty('params'); + expect(second).not.toHaveProperty('selectInclude'); + expect(second).not.toHaveProperty('alias'); + expect(second['expression']).toBe(`EXISTS ${subQuery}`); + }); + it('expression for target relation field with many-to-many', () => { + const query = getDefaultQuery(); + query.filter.target = { + roles: { + [FilterOperand.eq]: 'null', + [FilterOperand.ne]: 'null', + }, + }; + const subQuery = getQuery() + .where(`Users-Roles.user_id = Users.id`) + .getQuery(); + const result = + typeormUtilsServiceUser.getFilterExpressionForTarget(query); + + expect(result.length).toBe(2); + const [first, second] = result; + expect(first).not.toHaveProperty('params'); + expect(first).not.toHaveProperty('selectInclude'); + expect(first).not.toHaveProperty('alias'); + expect(first['expression']).toBe(`NOT EXISTS ${subQuery}`); + expect(second).not.toHaveProperty('params'); + expect(second).not.toHaveProperty('selectInclude'); + expect(second).not.toHaveProperty('alias'); + expect(second['expression']).toBe(`EXISTS ${subQuery}`); + }); + }); + + describe('getFilterExpressionForRelation', () => { + it('expression for relation many-to-many', () => { + const query = getDefaultQuery(); + const conditional = { + name: { + [FilterOperand.eq]: 'null', + [FilterOperand.ne]: 'null', + }, + createdAt: { + [FilterOperand.eq]: 'test1', + [FilterOperand.ne]: 'test2', + [FilterOperand.nin]: ['test3'] as [string, ...string[]], + }, + }; + + query.filter.relation = { + roles: conditional, + }; + + let subQuery = getQuery() + .where(`"Users__Roles_roles"."name" IS NULL`) + .andWhere(`"Users__Roles_roles"."name" IS NOT NULL`) + .andWhere(`"Users__Roles_roles"."created_at" = :param1`) + .andWhere(`"Users__Roles_roles"."created_at" <> :param2`) + .andWhere(`"Users__Roles_roles"."created_at" NOT IN (:...param3)`) + .getQuery(); + + const result = + typeormUtilsServiceUser.getFilterExpressionForRelation(query); + + expect(result.length).toBe(1); + + const [first] = result; + expect(first).not.toHaveProperty('selectInclude'); + if (!first.params && !Array.isArray(first.params)) { + expect(first).toHaveProperty('params'); + expect(first.params).toBeInstanceOf(Array); + } + if (Array.isArray(first.params)) { + expect(first?.params?.length).toBe(3); + const [firstParams, secondParams, thirdParams] = first.params; + expect(firstParams?.val).toBe( + query.filter?.relation?.roles?.createdAt?.eq + ); + + const regResult1 = firstParams?.name.match( + new RegExp(`params_Roles.createdAt_\\d{1,}`) + ); + if (regResult1) { + subQuery = subQuery.replace('param1', regResult1[0]); + } + expect(regResult1).not.toBe(null); + + expect(secondParams?.val).toBe( + query.filter?.relation?.roles?.createdAt?.ne + ); + + const regResult2 = secondParams?.name.match( + new RegExp(`params_Roles.createdAt_\\d{1,}`) + ); + if (regResult2) { + subQuery = subQuery.replace('param2', regResult2[0]); + } + expect(regResult2).not.toBe(null); + + expect(thirdParams?.val).toBe( + query.filter?.relation?.roles?.createdAt?.nin + ); + const regResult3 = thirdParams?.name.match( + new RegExp(`params_Roles.createdAt_\\d{1,}`) + ); + if (regResult3) { + subQuery = subQuery.replace('param3', regResult3[0]); + } + } + expect(first.alias).toBe(`Users.id`); + expect(first.expression).toBe(`IN ${subQuery}`); + }); + + it('expression for relation other type', () => { + const query = getDefaultQuery(); + query.filter.relation = { + addresses: { + createdAt: { + eq: 'qweqwe', + }, + }, + comments: { + createdAt: { + like: 'sdfsdf', + }, + }, + }; + const firstAlias = 'Addresses.createdAt'; + const secondAlias = 'Comments.createdAt'; + const result = + typeormUtilsServiceUser.getFilterExpressionForRelation(query); + + expect(result.length).toBe(2); + const [first, second] = result; + + const firstResult = first.expression.match( + new RegExp(`params_${firstAlias}_\\d{1,}`) + ); + + if (!firstResult) { + expect(firstResult).not.toBe(null); + throw Error('Should be like pattern'); + } + + expect(first.expression).toBe(`= :${firstResult[0]}`); + expect(first.alias).toBe(`Users__Addresses_addresses.createdAt`); + expect(first.selectInclude).toBe('addresses'); + if (!Array.isArray(first.params)) { + expect(first.params?.name).toBe(`${firstResult[0]}`); + expect(first.params?.val).toBe( + query.filter.relation?.addresses?.createdAt?.eq + ); + } else { + expect(first.params).not.toBeInstanceOf(Array); + } + + const secondResult = second.expression.match( + new RegExp(`params_${secondAlias}_\\d{1,}`) + ); + if (!secondResult) { + expect(secondResult).not.toBe(null); + throw Error('Should be like pattern'); + } + + expect(second.expression).toBe(`ILIKE :${secondResult[0]}`); + expect(second.alias).toBe('Users__Comments_comments.createdAt'); + expect(second.selectInclude).toBe('comments'); + if (!Array.isArray(second.params)) { + expect(second.params?.name).toBe(secondResult[0]); + expect(second.params?.val).toBe( + `%${query.filter.relation?.comments?.createdAt?.like}%` + ); + } else { + expect(second.params).not.toBeInstanceOf(Array); + } + }); + }); + + describe('validateRelationInputData', () => { + let usersData: Users; + beforeEach(async () => { + const result = await userRepository.findOne({ + where: { + id: 1, + }, + relations: { + roles: true, + userGroup: true, + manager: true, + }, + }); + if (!result) { + throw Error('not found mock data'); + } + usersData = result; + }); + it('should be ok', async () => { + const rolesData = usersData.roles.map((i) => ({ + type: 'roles', + id: i.id.toString(), + })); + + const userGroupData = { + type: 'user-groups', + id: usersData.userGroup.id.toString(), + }; + const managerData = { + type: 'users', + id: usersData.manager.id.toString(), + }; + const emptyRoles: { id: string; type: string }[] = []; + const emptyManager = null; + const result = await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + rolesData + ); + const result1 = await typeormUtilsServiceUser.validateRelationInputData( + 'userGroup', + userGroupData + ); + const result2 = await typeormUtilsServiceUser.validateRelationInputData( + 'manager', + managerData + ); + const result3 = await typeormUtilsServiceUser.validateRelationInputData( + 'manager', + emptyManager + ); + const result4 = await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + emptyRoles + ); + expect(result).toEqual(usersData.roles.map((i) => i.id.toString())); + expect(result1).toEqual(usersData.userGroup.id.toString()); + expect(result2).toEqual(usersData.manager.id.toString()); + expect(result3).toEqual(emptyManager); + expect(result4).toEqual(emptyRoles); + }); + + it('Should be error incorrect type name', async () => { + const rolesData = usersData.roles.map((i, index) => ({ + type: index === 1 ? 'other' : 'roles', + id: i.id.toString(), + })) as PostRelationshipData; + + const userGroupData = { + type: 'userGroups', + id: usersData.userGroup.id.toString(), + }; + const managerData = { + type: 'user', + id: usersData.manager.id.toString(), + }; + expect.assertions(3); + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + rolesData + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'userGroup', + userGroupData + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'manager', + managerData + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + }); + + it('Should be error, Incorrect relation type', async () => { + expect.assertions(2); + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + {} as any + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'userGroup', + [] as any + ); + } catch (e) { + expect(e).toBeInstanceOf(UnprocessableEntityException); + } + }); + + it('Should be error, Not fond', async () => { + const rolesData = usersData.roles.map((i, index) => ({ + type: 'roles', + id: index === 1 ? '1000' : i.id.toString(), + })) as PostRelationshipData; + expect.assertions(2); + try { + await typeormUtilsServiceUser.validateRelationInputData( + 'roles', + rolesData + ); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + try { + await typeormUtilsServiceUser.validateRelationInputData('userGroup', { + type: 'user-groups', + id: '10000', + }); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts new file mode 100644 index 00000000..9683bd32 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts @@ -0,0 +1,685 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { EntityMetadata, Equal, In, Repository } from 'typeorm'; +import { RelationMetadata as TypeOrmRelationMetadata } from 'typeorm/metadata/RelationMetadata'; +import { + camelToKebab, + kebabToCamel, + ObjectTyped, + snakeToCamel, + FilterOperand, +} from '@klerick/json-api-nestjs-shared'; + +import { + ObjectLiteral, + EXPRESSION, + OperandMapExpressionForNull, + OperandsMapExpression, + OperandsMapExpressionForNullRelation, + ValidateQueryError, +} from '../../../types'; + +import { PatchData, PostData, Query } from '../../mixin/zod'; +import { CURRENT_ENTITY_REPOSITORY } from '../constants'; +import { TupleOfEntityRelation, EntityRelation } from '../../mixin/types'; +import { getEntityName } from '../../mixin/helper'; + +type RelationAlias = { + [K in TupleOfEntityRelation[number]]: string; +}; +type RelationMetadata = { + [K in TupleOfEntityRelation[number]]: TypeOrmRelationMetadata; +}; + +type ResultQueryExpressionObject = { name: string; val: string }; +type ResultQueryExpressionArray = { name: string; val: string }[]; + +export type RelationshipsResult = { + [K in EntityRelation]: E[K] extends E[K][] ? E[K] : E[K] | null; +}; + +export type ResultQueryExpression = { + alias?: string; + expression: string; + paramsForResult?: string[]; + params?: ResultQueryExpressionObject | ResultQueryExpressionArray; + selectInclude?: string; +}; +export type InputValidateData = { + type: string; + id: string; +}; + +export type ValidateReturn = T extends unknown[] + ? string[] + : T extends null + ? null + : string; + +type Entity = ObjectLiteral; + +function isTargetField( + relationField: TupleOfEntityRelation, + field: any +): field is TupleOfEntityRelation[number] { + return relationField.includes(field); +} + +function isRelationField( + relationField: TupleOfEntityRelation, + field: any +): asserts field is EntityRelation { + if (isTargetField(relationField, field)) return; + const error: ValidateQueryError = { + code: 'unrecognized_keys', + path: ['data', 'relationships'], + message: `Resource for relation '${field.toString()}' does not exist`, + keys: [field], + }; + + throw new BadRequestException([error]); +} + +Injectable(); +export class TypeormUtilsService { + private readonly _currentAlias!: string; + private readonly _relationMetadata = {} as RelationMetadata; + private readonly _relationAlias = {} as RelationAlias; + private readonly _relationFields!: TupleOfEntityRelation; + private readonly _entityMetadata!: EntityMetadata; + private _number = 0; + + constructor( + @Inject(CURRENT_ENTITY_REPOSITORY) private repository: Repository + ) { + this._currentAlias = snakeToCamel(repository.metadata.name); + const relationFields = []; + for (const metadata of repository.metadata.relations) { + const propertyName = + metadata.propertyName as TupleOfEntityRelation[number]; + this._relationMetadata[propertyName] = metadata; + this._relationAlias[propertyName] = snakeToCamel( + metadata.inverseEntityMetadata.name + ); + relationFields.push(propertyName); + } + this._relationFields = relationFields as TupleOfEntityRelation; + this._entityMetadata = repository.metadata; + } + get currentAlias() { + return this._currentAlias; + } + + get relationFields() { + return this._relationFields; + } + + relationName(relName: TupleOfEntityRelation[number]) { + return this._relationAlias[relName]; + } + + get currentPrimaryColumn(): keyof E { + return this._entityMetadata.primaryColumns[0].propertyName as keyof E; + } + + getAliasForRelation(relName: TupleOfEntityRelation[number]) { + return `${this.currentAlias}__${this._relationAlias[relName]}_${relName}`; + } + + getRelMetaDataForRelation(relName: TupleOfEntityRelation[number]) { + return this._relationMetadata[relName]; + } + + getPrimaryColumnForRel(relName: TupleOfEntityRelation[number]) { + return this._relationMetadata[relName].inverseEntityMetadata + .primaryColumns[0].propertyName; + } + + private getFilterObject(query: Query, filterType: 'target' | 'relation') { + const { filter } = query; + if (!filter) return null; + return filter[filterType]; + } + + private get number() { + if (this._number > 1000) { + this._number = 0; + } + this._number++; + return this._number; + } + + private getParamName(fieldName: string) { + return `params_${fieldName}_${this.number}`; + } + + getAliasPath(fieldName: unknown): string; + getAliasPath( + fieldName: unknown, + relname: TupleOfEntityRelation[number], + separator?: string + ): string; + getAliasPath(fieldName: unknown, relname: string, separator?: string): string; + getAliasPath( + fieldName: unknown, + relname?: TupleOfEntityRelation[number] | string, + separator = '.' + ): string { + const alias = relname + ? this._relationAlias[relname] || relname + : this.currentAlias; + return `${alias}${separator}${fieldName}`; + } + + private getSubQueryForManyToMany( + fieldName: TupleOfEntityRelation[number], + expression?: string[] + ): string { + const metadataRelation: TypeOrmRelationMetadata = + this._relationMetadata[fieldName]; + const relationPrimaryColumn = + metadataRelation.inverseEntityMetadata.primaryColumns[0].propertyName; + const { joinTableName, inverseJoinColumns, joinColumns } = + metadataRelation.isManyToManyOwner + ? metadataRelation + : metadataRelation.inverseRelation || metadataRelation; + + const { databaseName: queryJoinPropsName } = + metadataRelation.isManyToManyOwner + ? inverseJoinColumns[0] + : joinColumns[0]; + const { databaseName: selectJoinPropsName } = + metadataRelation.isManyToManyOwner + ? joinColumns[0] + : inverseJoinColumns[0]; + + const alias = this.getAliasPath( + this._relationAlias[fieldName], + this.currentAlias, + '-' + ); + + const selectAlias = this.getAliasPath(selectJoinPropsName, alias); + + const query = this.repository + .createQueryBuilder() + .subQuery() + .select(selectAlias) + .from(joinTableName, alias) + .leftJoin( + this._relationMetadata[fieldName].inverseEntityMetadata.target, + this.getAliasForRelation(fieldName), + `${this.getAliasPath(queryJoinPropsName, alias)} = ${this.getAliasPath( + relationPrimaryColumn, + this.getAliasForRelation(fieldName) + )}` + ); + if (!expression) { + query.where( + `${selectAlias} = ${this.getAliasPath(this.currentPrimaryColumn)}` + ); + } else { + for (const i in expression) { + query[i === '0' ? 'where' : 'andWhere'](expression[i]); + } + } + return query.getQuery(); + } + + getFilterExpressionForTarget(query: Query): ResultQueryExpression[] { + const resultExpression: ResultQueryExpression[] = []; + const filterTarget = this.getFilterObject(query, 'target'); + if (!filterTarget) return resultExpression; + for (const [fieldName, filter] of ObjectTyped.entries(filterTarget)) { + if (!filter) continue; + for (const entries of ObjectTyped.entries(filter)) { + const [operand, value] = entries as [FilterOperand, string]; + const valueConditional = + operand === FilterOperand.like ? `%${value}%` : value; + const fieldWithAlias = this.getAliasPath(fieldName); + const paramsName = this.getParamName(fieldWithAlias); + + if (!isTargetField(this._relationFields, fieldName)) { + if ( + (operand === FilterOperand.ne || operand === FilterOperand.eq) && + (valueConditional === 'null' || valueConditional === null) + ) { + const expression = OperandMapExpressionForNull[operand].replace( + EXPRESSION, + paramsName + ); + resultExpression.push({ + alias: fieldWithAlias, + expression, + }); + continue; + } + + const expression = OperandsMapExpression[operand].replace( + EXPRESSION, + paramsName + ); + resultExpression.push({ + alias: fieldWithAlias, + expression, + params: { + val: valueConditional, + name: paramsName, + }, + }); + continue; + } + + const metadataRelation: TypeOrmRelationMetadata = + this._relationMetadata[fieldName]; + const relationTarget = metadataRelation.inverseEntityMetadata.target; + const relationAlias = this._relationAlias[fieldName]; + const subQuery = this.repository.createQueryBuilder().subQuery(); + + const resultOperand = + operand === FilterOperand.eq ? operand : FilterOperand.ne; + switch (metadataRelation.relationType) { + case 'many-to-many': { + const subQuerySql = this.getSubQueryForManyToMany(fieldName); + + const resultOperand = + operand === FilterOperand.eq ? operand : FilterOperand.ne; + + const expression = OperandsMapExpressionForNullRelation[ + resultOperand + ].replace(EXPRESSION, subQuerySql); + + resultExpression.push({ + expression, + }); + break; + } + case 'one-to-many': { + const joinColumn = metadataRelation.inverseSidePropertyPath; + + const aliasPath = this.getAliasPath(joinColumn, fieldName); + const subQuerySql = subQuery + .select(aliasPath, joinColumn) + .from(relationTarget, relationAlias) + .where( + `${aliasPath} = ${this.getAliasPath(this.currentPrimaryColumn)}` + ) + .getQuery(); + + const expression = OperandsMapExpressionForNullRelation[ + resultOperand + ].replace(EXPRESSION, subQuerySql); + + resultExpression.push({ + expression, + }); + break; + } + default: { + const expression = OperandMapExpressionForNull[ + resultOperand + ].replace(EXPRESSION, paramsName); + resultExpression.push({ + alias: fieldWithAlias, + expression, + }); + } + } + } + } + + return resultExpression; + } + + getFilterExpressionForRelation(query: Query): ResultQueryExpression[] { + const resultExpression: ResultQueryExpression[] = []; + const filterRelation = this.getFilterObject(query, 'relation'); + if (!filterRelation) return resultExpression; + + for (const [relationField, propsFilter] of ObjectTyped.entries( + filterRelation + )) { + if (!propsFilter) continue; + if (!isTargetField(this._relationFields, relationField)) continue; + const metadataRelation: TypeOrmRelationMetadata = + this._relationMetadata[relationField]; + + const conditionalForManyToMany: { + conditional: string; + params: { name: string; val: string } | undefined; + }[] = []; + + for (const [relationFieldProps, filter] of ObjectTyped.entries( + propsFilter + )) { + if (!filter) continue; + + for (const entries of ObjectTyped.entries(filter)) { + const [operand, value] = entries as [FilterOperand, string]; + const currentValue = + operand === FilterOperand.like ? `%${value}%` : value; + + const paramsName = this.getParamName( + this.getAliasPath(relationFieldProps.toString(), relationField) + ); + let expression: string; + if (value === 'null') { + const currentOperand = + operand === FilterOperand.eq + ? FilterOperand.eq + : FilterOperand.ne; + expression = OperandMapExpressionForNull[currentOperand]; + } else { + expression = OperandsMapExpression[operand].replace( + EXPRESSION, + paramsName + ); + } + + const params = + value === 'null' + ? undefined + : { + val: currentValue, + name: paramsName, + }; + + switch (metadataRelation.relationType) { + case 'many-to-many': { + conditionalForManyToMany.push({ + params, + conditional: `${this.getAliasPath( + relationFieldProps.toString(), + this.getAliasForRelation(relationField) + )} ${expression}`, + }); + + break; + } + default: { + resultExpression.push({ + alias: this.getAliasPath( + relationFieldProps.toString(), + this.getAliasForRelation(relationField) + ), + expression, + selectInclude: relationField, + params, + }); + } + } + } + } + + if (conditionalForManyToMany.length) { + const expression = conditionalForManyToMany.map((i) => i.conditional); + const subQuery = this.getSubQueryForManyToMany( + relationField, + expression + ); + + const mainExpression = `IN ${subQuery}`; + + const params = conditionalForManyToMany + .filter((i) => i.params) + .map((i) => i.params) as { name: string; val: string }[]; + resultExpression.push({ + alias: this.getAliasPath(this.currentPrimaryColumn), + expression: mainExpression, + paramsForResult: expression, + params, + }); + } + } + return resultExpression; + } + + private throwError(message: string, path: string[], key?: string) { + const error: ValidateQueryError = { + code: 'unrecognized_keys', + path, + message, + }; + if (key) { + error.keys = [key]; + } + throw new BadRequestException([error]); + } + + async *asyncIterateFindRelationships( + relationships: NonNullable< + PatchData['relationships'] | PostData['relationships'] + > + ): AsyncGenerator> { + for (const entries of ObjectTyped.entries(relationships)) { + const [props, dataItem] = entries; + + isRelationField(this._relationFields, props); + if (dataItem === undefined) continue; + + const { data } = dataItem; + if (data === undefined) continue; + if (data === null) { + yield { [props]: null } as RelationshipsResult; + continue; + } + const isArray = Array.isArray(data); + if (isArray && data.length === 0) { + yield { [props]: [] } as RelationshipsResult; + continue; + } + + const condition = isArray + ? In((data as any[]).map((i) => i.id)) + : Equal(data['id']); + const relationsTypeName = kebabToCamel( + isArray ? (data as any[])[0]['type'] : data['type'] + ); + const primaryField = this.getPrimaryColumnForRel( + props as TupleOfEntityRelation[number] + ); + const relationsTarget = + this._relationMetadata[props as TupleOfEntityRelation[number]] + .inverseEntityMetadata.target; + const result = await this.repository.manager + .getRepository(relationsTarget) + .find({ + select: { + [primaryField]: true, + }, + where: { + [primaryField]: condition, + }, + }); + + if ( + (isArray && (result.length === 0 || data.length !== result.length)) || + (!isArray && result.length === 0) + ) { + const message = isArray + ? `Resource '${relationsTypeName}' with ids '${(data as any[]) + .map((i) => i.id) + .filter((i) => !result.find((r) => r[primaryField] == i)) + .join(',')}' does not exist` + : `Resource '${relationsTypeName}' with id '${data.id}' does not exist`; + + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data', 'relationships', props.toString()], + message, + }; + + throw new BadRequestException([error]); + } + + yield { [props]: isArray ? result : result[0] } as RelationshipsResult; + } + } + + async saveEntityData( + target: E, + relationships: PatchData['relationships'] | PostData['relationships'] + ): Promise { + if (relationships) { + for await (const item of this.asyncIterateFindRelationships( + relationships + )) { + const [props, type] = ObjectTyped.entries(item)[0]; + if (type !== null) { + target[props] = type as any; + } else { + target[props] = null as any; + } + } + } + const saveData = await this.repository.save(target); + let saveDataWithRelation: E | null = null; + if (relationships) { + const queryBuilder = this.repository + .createQueryBuilder(this.currentAlias) + .where({ + [this.currentPrimaryColumn]: Equal( + saveData[this.currentPrimaryColumn] + ), + }); + + for (const [props] of ObjectTyped.entries(relationships)) { + const currentIncludeAlias = this.getAliasForRelation(props.toString()); + + queryBuilder.leftJoinAndSelect( + this.getAliasPath(props), + currentIncludeAlias + ); + } + + saveDataWithRelation = await queryBuilder.getOne(); + } + + return saveDataWithRelation ? saveDataWithRelation : saveData; + } + + async validateRelationInputData< + Rel extends EntityRelation, + In extends InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends null | InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends null | InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise> { + const relationMetadata = + this._relationMetadata[rel as TupleOfEntityRelation[number]]; + const isArray = Array.isArray(inputData); + + if ( + ['one-to-many', 'many-to-many'].includes(relationMetadata.relationType) && + !isArray + ) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data'], + message: 'Body data should be array', + }; + + throw new UnprocessableEntityException([error]); + } + + if ( + ['one-to-one', 'many-to-one'].includes(relationMetadata.relationType) && + isArray + ) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data'], + message: 'Body data should be object', + }; + + throw new UnprocessableEntityException([error]); + } + if (inputData === null) { + const result = null; + return result as ValidateReturn; + } + if (isArray && inputData.length === 0) { + const result: any[] = []; + return result as ValidateReturn; + } + + const prepareData = isArray ? inputData : [inputData]; + + const errors: ValidateQueryError[] = []; + let i = 0; + const typeName = camelToKebab( + getEntityName(relationMetadata.inverseEntityMetadata.target) + ); + + for (const prepareItem of prepareData) { + if (prepareItem.type !== typeName) { + const path = isArray ? ['data', i.toString()] : ['data']; + errors.push({ + code: 'invalid_arguments', + path: path, + message: `Type should be equal to type of relName: "${rel.toString()}". Type of ${rel.toString()} is "${typeName}" but receive - "${ + prepareItem.type + }"`, + }); + } + i++; + } + if (errors.length) { + throw new UnprocessableEntityException(errors); + } + + const checkResult = await this.repository.manager + .getRepository(relationMetadata.inverseEntityMetadata.target) + .find({ + select: { + [this.getPrimaryColumnForRel(rel.toString())]: true, + }, + where: { + [this.getPrimaryColumnForRel(rel.toString())]: In( + prepareData.map((i) => i.id) + ), + }, + }); + + if (checkResult.length === prepareData.length) { + return ( + isArray ? inputData.map((i) => i.id) : inputData.id + ) as ValidateReturn; + } + + const resulDataMap = checkResult.reduce((acum, item) => { + acum[item[this.getPrimaryColumnForRel(rel.toString())]] = true; + return acum; + }, {} as Record); + + i = 0; + for (const item of prepareData) { + if (!resulDataMap[item.id]) { + const path = isArray ? ['data', i.toString(), 'id'] : ['data', 'id']; + errors.push({ + code: 'invalid_arguments', + path: path, + message: `Not exist item "${ + item.id + }" in relation "${rel.toString()}"`, + }); + } + i++; + } + throw new NotFoundException(errors); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts new file mode 100644 index 00000000..715d1c86 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts @@ -0,0 +1,67 @@ +import { DynamicModule } from '@nestjs/common'; +import { TypeOrmModule as MainTypeOrmModule } from '@nestjs/typeorm'; +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; + +import { NestProvider, ObjectLiteral, ResultModuleOptions } from '../../types'; +import { + CurrentEntityManager, + CurrentDataSourceProvider, + ZodParamsFactory, + CurrentEntityRepository, + FindOneRowEntityFactory, + CheckRelationNameFactory, + OrmServiceFactory, + GetFieldForEntity, + RunInTransactionFactory, +} from './factory'; +import { + EntityPropsMapService, + TransformDataService, + TypeormUtilsService, +} from './service'; +import { GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; + +export class TypeOrmModule { + static forRoot(options: ResultModuleOptions): DynamicModule { + const optionProvider = { + provide: GLOBAL_MODULE_OPTIONS_TOKEN, + useValue: options, + }; + + const typeOrmModule = MainTypeOrmModule.forFeature( + options.entities as EntityClassOrSchema[], + options.connectionName + ); + + const currentProvider = [ + ...(options.providers || []), + optionProvider, + CurrentDataSourceProvider(options.connectionName), + CurrentEntityManager(), + GetFieldForEntity(), + EntityPropsMapService, + RunInTransactionFactory(), + ]; + + const currentImport = [typeOrmModule, ...(options.imports || [])]; + + return { + module: TypeOrmModule, + imports: currentImport, + providers: currentProvider, + exports: [...currentProvider, ...currentImport], + }; + } + + static getUtilProviders(entity: ObjectLiteral): NestProvider { + return [ + CurrentEntityRepository(entity), + TransformDataService, + TypeormUtilsService, + ZodParamsFactory(), + OrmServiceFactory(), + FindOneRowEntityFactory(), + CheckRelationNameFactory(), + ]; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts new file mode 100644 index 00000000..7494c8e4 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts @@ -0,0 +1,11 @@ +import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; +import { EntityTarget, ObjectLiteral } from '../../types'; +import { ResultGetField } from '../mixin/types'; + +export type TypeOrmParam = { + useSoftDelete?: boolean; + runInTransaction?: any>( + isolationLevel: IsolationLevel, + fn: Func + ) => ReturnType; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/config-param.ts b/libs/json-api/json-api-nestjs/src/lib/types/config-param.ts new file mode 100644 index 00000000..4d3c05e5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/config-param.ts @@ -0,0 +1,45 @@ +import { + AnyEntity, + EntityName, + NestController, + NestImport, + NestProvider, + PipeMixin, +} from './util-types'; +import { NonEmptyArray } from 'zod-validation-error'; +import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; + +export type ExtractNestType = + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; + +export type ConfigParam = { + requiredSelectField: boolean; + debug: boolean; + pipeForId: PipeMixin; + operationUrl: string; + overrideRoute: string; +}; + +export type GeneralParam = { + connectionName?: string; + entities: NonEmptyArray>; + controllers?: NestController; + providers?: NestProvider; + imports?: NestImport; +}; + +export type ResultGeneralParam = { + connectionName: string; + entities: NonEmptyArray>; + controllers: NestController; + providers: NestProvider; + imports: NestImport; +}; + +export interface BaseModuleOptions { + entity: EntityClassOrSchema; + connectionName: string; + controller?: ExtractNestType; + config: ConfigParam; + imports?: NestImport; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts new file mode 100644 index 00000000..ca9e3564 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/error.types.ts @@ -0,0 +1,18 @@ +import { ZodIssue } from 'zod'; + +export type InnerErrorType = + | 'invalid_arguments' + | 'unrecognized_keys' + | 'internal_error'; + +export type InnerError = { + code: InnerErrorType; + message: string; + path: string[]; + keys?: string[]; + error?: Error; +}; + +export type ValidateQueryError = ZodIssue | InnerError; + +export type ErrorDescribe = ValidateQueryError; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/types/index.ts new file mode 100644 index 00000000..0ad37ef9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/index.ts @@ -0,0 +1,5 @@ +export * from './config-param'; +export * from './module-common.types'; +export * from './util-types'; +export * from './error.types'; +export * from './operand'; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts new file mode 100644 index 00000000..8c83dd54 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts @@ -0,0 +1,29 @@ +import { + MicroOrmModule, + TypeOrmModule, + TypeOrmParam, + MicroOrmParam, +} from '../modules'; + +import { ConfigParam, GeneralParam, ResultGeneralParam } from './config-param'; +import { RequiredFromPartial } from './util-types'; + +export type ModuleOptions = + | (GeneralParam & { + type: typeof MicroOrmModule; + options: Partial; + }) + | (GeneralParam & { + type?: typeof TypeOrmModule; + options: Partial; + }); + +export type ResultModuleOptions = + | (ResultGeneralParam & { + type: typeof MicroOrmModule; + options: RequiredFromPartial; + }) + | (ResultGeneralParam & { + type: typeof TypeOrmModule; + options: RequiredFromPartial; + }); diff --git a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts b/libs/json-api/json-api-nestjs/src/lib/types/operand.ts new file mode 100644 index 00000000..c54b9985 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/operand.ts @@ -0,0 +1,26 @@ +import { FilterOperand } from '@klerick/json-api-nestjs-shared'; + +export const EXPRESSION = 'EXPRESSION'; +export const OperandsMapExpression = { + [FilterOperand.eq]: `= :${EXPRESSION}`, + [FilterOperand.ne]: `<> :${EXPRESSION}`, + [FilterOperand.regexp]: `~* :${EXPRESSION}`, + [FilterOperand.gt]: `> :${EXPRESSION}`, + [FilterOperand.gte]: `>= :${EXPRESSION}`, + [FilterOperand.in]: `IN (:...${EXPRESSION})`, + [FilterOperand.like]: `ILIKE :${EXPRESSION}`, + [FilterOperand.lt]: `< :${EXPRESSION}`, + [FilterOperand.lte]: `<= :${EXPRESSION}`, + [FilterOperand.nin]: `NOT IN (:...${EXPRESSION})`, + [FilterOperand.some]: `&& :${EXPRESSION}`, +}; + +export const OperandMapExpressionForNull = { + [FilterOperand.ne]: 'IS NOT NULL', + [FilterOperand.eq]: 'IS NULL', +}; + +export const OperandsMapExpressionForNullRelation = { + [FilterOperand.ne]: `EXISTS ${EXPRESSION}`, + [FilterOperand.eq]: `NOT EXISTS ${EXPRESSION}`, +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/util-types.ts b/libs/json-api/json-api-nestjs/src/lib/types/util-types.ts new file mode 100644 index 00000000..1954c168 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/types/util-types.ts @@ -0,0 +1,24 @@ +import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common'; +import { EntityTarget as EntityTargetTypeOrm } from 'typeorm/common/EntityTarget'; + +export type AnyEntity = Partial; + +export type EntityClass = Function & { prototype: T }; +export type EntityName = EntityClass; +export type EntityTarget = EntityClass | EntityTargetTypeOrm; +export interface ObjectLiteral { + [key: string]: any; +} + +export type NestController = NonNullable; +export type NestProvider = NonNullable; +export type NestImport = NonNullable; +export type PipeMixin = Type; + +export type RequiredFromPartial = { + [P in keyof T]-?: T[P] extends infer U | undefined ? U | false : T[P]; +}; + +export type RunInTransaction< + F extends (...arg: any[]) => Promise = (...arg: any[]) => Promise +> = (arg: F) => ReturnType; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts new file mode 100644 index 00000000..ca67bf72 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts @@ -0,0 +1,152 @@ +import { DynamicModule, ParseIntPipe } from '@nestjs/common'; + +import { MicroOrmModule, TypeOrmModule, TypeOrmParam } from '../modules'; +import { ConfigParam, RequiredFromPartial } from '../types'; +import { prepareConfig, createMixinModule } from './helper'; +import { + DEFAULT_CONNECTION_NAME, + JSON_API_CONTROLLER_POSTFIX, +} from '../constants'; +import { JsonBaseController } from '../modules/mixin/controller/json-base.controller'; +import { JsonApi } from '../modules/mixin/decorators'; +import { getProviderName } from '../modules/mixin/helper'; + +class A {} +describe('Helper tests', () => { + describe('prepareConfig', () => { + it('should return default config when type is undefined', () => { + const result = prepareConfig({ + entities: [A], + options: { debug: false, requiredSelectField: false }, + }); + + expect(Array.isArray(result.imports)).toBe(true); + expect(Array.isArray(result.controllers)).toBe(true); + expect(Array.isArray(result.providers)).toBe(true); + expect(result.type).toBe(TypeOrmModule); + expect(result.options.debug).toBe(false); + expect(result.options.requiredSelectField).toBe(false); + expect(result.connectionName).toBe(DEFAULT_CONNECTION_NAME); + }); + + it('should return TypeOrm config when type is TypeOrmModule', () => { + const result = prepareConfig({ + entities: [A], + type: TypeOrmModule, + options: { + debug: true, + requiredSelectField: true, + useSoftDelete: true, + }, + }); + + expect(result.type).toBe(TypeOrmModule); + expect(result.options.debug).toBe(true); + expect( + (result.options as RequiredFromPartial) + .useSoftDelete + ).toBe(true); + }); + + it('should return MicroOrm config when type is MicroOrmModule', () => { + const result = prepareConfig({ + entities: [A], + type: MicroOrmModule, + options: { debug: true, requiredSelectField: true }, + }); + + expect(result.type).toBe(MicroOrmModule); + expect(result.options.debug).toBe(true); + expect(result.options.requiredSelectField).toBe(true); + + // @ts-expect-error eed check run time + expect((result.options as ConfigParam).useSoftDelete).toBeUndefined(); + }); + + it('should use default values for pipeForId, operationUrl, and overrideRoute when not provided', () => { + const result = prepareConfig({ + entities: [A], + options: {}, + }); + + expect(result.options.pipeForId).toBe(ParseIntPipe); + expect(result.options.operationUrl).toBe(false); + expect(result.options.overrideRoute).toBe(false); + }); + }); + + describe('createMixinModule', () => { + it('should create a MixinModule with the correct controller matching the entity', () => { + class TestEntity {} + @JsonApi(TestEntity) + class TestController extends JsonBaseController {} + const commonOrmModule = {} as DynamicModule; + const resultOptions = prepareConfig({ + entities: [TestEntity], + controllers: [TestController], + connectionName: DEFAULT_CONNECTION_NAME, + type: TypeOrmModule, + options: { + debug: true, + requiredSelectField: true, + useSoftDelete: true, + }, + }); + + const result = createMixinModule( + TestEntity, + resultOptions, + commonOrmModule + ); + + expect(result).toHaveProperty('controllers', [TestController]); + expect(result).toHaveProperty('providers'); + expect(result.imports?.includes(commonOrmModule)).toBe(true); + }); + + it('should use undefined as controller if none match the entity', () => { + class TestEntity {} + const commonOrmModule = {} as DynamicModule; + const resultOptions = prepareConfig({ + entities: [TestEntity], + controllers: [], + connectionName: 'test_connection', + options: { debug: false }, + imports: [], + }); + + const result = createMixinModule( + TestEntity, + resultOptions, + commonOrmModule + ); + + const controller = (result.controllers || []).at(0); + expect(controller?.name).toBe( + getProviderName(TestEntity, JSON_API_CONTROLLER_POSTFIX) + ); + }); + + it('should correctly construct the MixinModule using given ResultModuleOptions', () => { + class AnotherEntity {} + class SharedModule {} + const commonOrmModule = {} as DynamicModule; + const importTest = { module: SharedModule }; + + const resultOptions = prepareConfig({ + entities: [AnotherEntity], + controllers: [], + connectionName: 'default_connection', + options: { debug: true, useSoftDelete: true }, + imports: [importTest], + }); + + const result = createMixinModule( + AnotherEntity, + resultOptions, + commonOrmModule + ); + expect(result.imports?.at(1)).toEqual(importTest); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts b/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts new file mode 100644 index 00000000..aa2154d5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts @@ -0,0 +1,120 @@ +import { DynamicModule, ParseIntPipe } from '@nestjs/common'; + +import { + AnyEntity, + ConfigParam, + EntityName, + ModuleOptions, + RequiredFromPartial, + ResultModuleOptions, +} from '../types'; +import { + DEFAULT_CONNECTION_NAME, + JSON_API_DECORATOR_ENTITY, +} from '../constants'; +import { + MicroOrmParam, + TypeOrmParam, + MicroOrmModule, + TypeOrmModule, + AtomicOperationModule, +} from '../modules'; +import { MixinModule } from '../modules/mixin/mixin.module'; +import { Type } from '@nestjs/common/interfaces'; +import { RouterModule } from '@nestjs/core'; + +export function prepareConfig( + moduleOptions: ModuleOptions +): ResultModuleOptions { + const { options: inputOptions } = moduleOptions; + + let resulOptions: + | RequiredFromPartial + | RequiredFromPartial; + let resulType: typeof TypeOrmModule | typeof MicroOrmModule; + const configParam: RequiredFromPartial = { + debug: !!inputOptions.debug, + requiredSelectField: !!inputOptions.requiredSelectField, + operationUrl: inputOptions.operationUrl || false, + overrideRoute: inputOptions.overrideRoute || false, + pipeForId: inputOptions.pipeForId || ParseIntPipe, + }; + + moduleOptions.type = moduleOptions.type || TypeOrmModule; + + if (moduleOptions.type === TypeOrmModule) { + const { runInTransaction, useSoftDelete } = + moduleOptions.options as Partial; + + resulType = TypeOrmModule; + resulOptions = { + ...configParam, + useSoftDelete: useSoftDelete ? useSoftDelete : false, + runInTransaction: runInTransaction ? runInTransaction : false, + } as ConfigParam & RequiredFromPartial; + } else { + resulType = MicroOrmModule; + resulOptions = { + ...configParam, + }; + } + + return { + connectionName: moduleOptions.connectionName || DEFAULT_CONNECTION_NAME, + entities: moduleOptions.entities, + imports: moduleOptions.imports || [], + providers: moduleOptions.providers || [], + controllers: moduleOptions.controllers || [], + type: resulType, + options: resulOptions, + } satisfies ResultModuleOptions; +} + +export function createMixinModule( + entity: EntityName, + resultOption: ResultModuleOptions, + commonOrmModule: DynamicModule +): DynamicModule { + const controller = (resultOption.controllers || []).find( + (item) => + item && Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, item) === entity + ); + + return MixinModule.forRoot({ + entity, + controller, + config: resultOption.options, + imports: [commonOrmModule, ...resultOption.imports], + ormModule: resultOption.type, + }); +} + +export function createAtomicModule( + options: ResultModuleOptions, + entitiesMixinModules: DynamicModule[], + commonOrmModule: DynamicModule +): DynamicModule[] { + const { operationUrl } = options.options; + if (!operationUrl) return []; + + return [ + AtomicOperationModule.forRoot( + { + ...options, + connectionName: options.connectionName, + }, + entitiesMixinModules, + commonOrmModule + ), + RouterModule.register([ + { + module: AtomicOperationModule, + path: operationUrl, + }, + ]), + ]; +} + +export function entityForClass(type: Type): EntityName { + return Reflect.getMetadata(JSON_API_DECORATOR_ENTITY, type); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/utils/index.ts new file mode 100644 index 00000000..d6d6f056 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/index.ts @@ -0,0 +1 @@ +export * from './helper'; diff --git a/migrations.json b/migrations.json new file mode 100644 index 00000000..a153a5fe --- /dev/null +++ b/migrations.json @@ -0,0 +1,142 @@ +{ + "migrations": [ + { + "version": "20.0.0-beta.7", + "description": "Migration for v20.0.0-beta.7", + "implementation": "./src/migrations/update-20-0-0/move-use-daemon-process", + "package": "nx", + "name": "move-use-daemon-process" + }, + { + "version": "20.0.1", + "description": "Set `useLegacyCache` to true for migrating workspaces", + "implementation": "./src/migrations/update-20-0-1/use-legacy-cache", + "x-repair-skip": true, + "package": "nx", + "name": "use-legacy-cache" + }, + { + "cli": "nx", + "version": "20.0.0-beta.5", + "description": "replace getJestProjects with getJestProjectsAsync", + "implementation": "./src/migrations/update-20-0-0/replace-getJestProjects-with-getJestProjectsAsync", + "package": "@nx/jest", + "name": "replace-getJestProjects-with-getJestProjectsAsync" + }, + { + "version": "20.2.0-beta.5", + "description": "Update TypeScript ESLint packages to v8.13.0 if they are already on v8", + "implementation": "./src/migrations/update-20-2-0/update-typescript-eslint-v8-13-0", + "package": "@nx/eslint", + "name": "update-typescript-eslint-v8.13.0" + }, + { + "cli": "nx", + "version": "19.6.1-beta.0", + "description": "Ensure Target Defaults are set correctly for Module Federation.", + "factory": "./src/migrations/update-19-6-1/ensure-depends-on-for-mf", + "package": "@nx/angular", + "name": "update-19-6-1-ensure-module-federation-target-defaults" + }, + { + "cli": "nx", + "version": "20.2.0-beta.2", + "description": "Update the ModuleFederationConfig import use @nx/module-federation.", + "factory": "./src/migrations/update-20-2-0/migrate-mf-imports-to-new-package", + "package": "@nx/angular", + "name": "update-20-2-0-update-module-federation-config-import" + }, + { + "cli": "nx", + "version": "20.2.0-beta.2", + "description": "Update the withModuleFederation import use @nx/module-federation/angular.", + "factory": "./src/migrations/update-20-2-0/migrate-with-mf-import-to-new-package", + "package": "@nx/angular", + "name": "update-20-2-0-update-with-module-federation-import" + }, + { + "cli": "nx", + "version": "20.2.0-beta.5", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Update the @angular/cli package version to ~19.0.0.", + "factory": "./src/migrations/update-20-2-0/update-angular-cli", + "package": "@nx/angular", + "name": "update-angular-cli-version-19-0-0" + }, + { + "cli": "nx", + "version": "20.2.0-beta.5", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Add the '@angular/localize/init' polyfill to the 'polyfills' option of targets using esbuild-based executors.", + "factory": "./src/migrations/update-20-2-0/add-localize-polyfill-to-targets", + "package": "@nx/angular", + "name": "add-localize-polyfill-to-targets" + }, + { + "cli": "nx", + "version": "20.2.0-beta.5", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Update '@angular/ssr' import paths to use the new '/node' entry point when 'CommonEngine' is detected.", + "factory": "./src/migrations/update-20-2-0/update-angular-ssr-imports-to-use-node-entry-point", + "package": "@nx/angular", + "name": "update-angular-ssr-imports-to-use-node-entry-point" + }, + { + "cli": "nx", + "version": "20.2.0-beta.6", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Disable the Angular ESLint prefer-standalone rule if not set.", + "factory": "./src/migrations/update-20-2-0/disable-angular-eslint-prefer-standalone", + "package": "@nx/angular", + "name": "disable-angular-eslint-prefer-standalone" + }, + { + "cli": "nx", + "version": "20.2.0-beta.8", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Remove Angular ESLint rules that were removed in v19.0.0.", + "factory": "./src/migrations/update-20-2-0/remove-angular-eslint-rules", + "package": "@nx/angular", + "name": "remove-angular-eslint-rules" + }, + { + "cli": "nx", + "version": "20.2.0-beta.8", + "requires": { "@angular/core": ">=19.0.0" }, + "description": "Remove the deprecated 'tailwindConfig' option from ng-packagr executors. Tailwind CSS configurations located at the project or workspace root will be picked up automatically.", + "factory": "./src/migrations/update-20-2-0/remove-tailwind-config-from-ng-packagr-executors", + "package": "@nx/angular", + "name": "remove-tailwind-config-from-ng-packagr-executors" + }, + { + "cli": "nx", + "version": "19.6.3-beta.0", + "description": "Migrate proxy config files to match new format from webpack-dev-server v5.", + "implementation": "./src/migrations/update-19-6-3/proxy-config", + "package": "@nx/webpack", + "name": "update-19-6-3-proxy-config" + }, + { + "version": "19.0.0", + "description": "Updates non-standalone Directives, Component and Pipes to 'standalone:false' and removes 'standalone:true' from those who are standalone", + "factory": "./bundles/explicit-standalone-flag#migrate", + "package": "@angular/core", + "name": "explicit-standalone-flag" + }, + { + "version": "19.0.0", + "description": "Updates ExperimentalPendingTasks to PendingTasks", + "factory": "./bundles/pending-tasks#migrate", + "package": "@angular/core", + "name": "pending-tasks" + }, + { + "version": "19.0.0", + "description": "Replaces `APP_INITIALIZER`, `ENVIRONMENT_INITIALIZER` & `PLATFORM_INITIALIZER` respectively with `provideAppInitializer`, `provideEnvironmentInitializer` & `providePlatformInitializer`.", + "factory": "./bundles/provide-initializer#migrate", + "optional": true, + "package": "@angular/core", + "name": "provide-initializer" + } + ] +} diff --git a/project.json b/project.json new file mode 100644 index 00000000..5b9f7ecd --- /dev/null +++ b/project.json @@ -0,0 +1,14 @@ +{ + "name": "@nestjs-json-api/source", + "$schema": "node_modules/nx/schemas/project-schema.json", + "targets": { + "local-registry": { + "executor": "@nx/js:verdaccio", + "options": { + "port": 4873, + "config": ".verdaccio/config.yml", + "storage": "tmp/local-registry/storage" + } + } + } +} From 750f685a9b4f8341abb08e19c276d74685168e3f Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 06:59:32 +0100 Subject: [PATCH 08/26] refactor(json-api-nestjs-sdk): Rename npm package and fox data BREAKING CHANGE: Rename npm package from json-api-nestjs-sdk to @klerick/json-api-nestjs-sdk. Now relation body params allow only as specification. https://jsonapi.org/format/#crud-updating-resource-relationships befoare allow without data props --- .../json-api/json-api-sdk/atomic-sdk.spec.ts | 2 +- .../check-common-decorator.spec.ts | 2 +- .../json-api-sdk/check-othe-call.spec.ts | 2 +- .../json-api/json-api-sdk/get-method.spec.ts | 3 +- .../json-api-sdk/patch-methode.spec.ts | 2 +- .../json-api/json-api-sdk/post-method.spec.ts | 2 +- .../src/json-api/utils/run-application.ts | 2 +- libs/json-api/json-api-nestjs-sdk/README.md | 10 +- .../src/lib/types/entity.ts | 2 +- .../src/lib/types/filter-operand.ts | 2 +- .../src/lib/types/response-body.ts | 2 +- .../src/lib/types/utils.ts | 2 +- .../lib/utils/generate-atomic-body.spec.ts | 28 +- .../src/lib/utils/index.ts | 4 +- nx.json | 1 + package-lock.json | 484 ++++++++++++++++-- package.json | 10 +- tsconfig.base.json | 21 +- 18 files changed, 486 insertions(+), 95 deletions(-) diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts index bb5fe060..9551718b 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts @@ -1,5 +1,5 @@ import { INestApplication } from '@nestjs/common'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { Addresses, CommentKind, Comments, Roles, Users } from 'database'; import { faker } from '@faker-js/faker'; import { getUser } from '../utils/data-utils'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts index 47b18d9f..91eb763e 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts @@ -1,5 +1,5 @@ import { INestApplication } from '@nestjs/common'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { AxiosError } from 'axios'; import { Users } from 'database'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts index 076dc277..abf3c40d 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts @@ -1,5 +1,5 @@ import { INestApplication } from '@nestjs/common'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { BookList, Users } from 'database'; import { AxiosError } from 'axios'; import { faker } from '@faker-js/faker'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts index 04889c41..d64f62cb 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts @@ -2,10 +2,9 @@ import { INestApplication } from '@nestjs/common'; import { Addresses, CommentKind, Comments, Roles, Users } from 'database'; import { faker } from '@faker-js/faker'; -import { FilterOperand, JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { getUser } from '../utils/data-utils'; import { creatSdk, run } from '../utils/run-application'; -import { AxiosError } from 'axios'; let app: INestApplication; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts index 62deb265..720828d9 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts @@ -1,7 +1,7 @@ import { INestApplication } from '@nestjs/common'; import { Addresses, CommentKind, Comments, Users } from 'database'; import { faker } from '@faker-js/faker'; -import { JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { creatSdk, run } from '../utils/run-application'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts index ebc9b484..e20b86d9 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts @@ -1,6 +1,6 @@ import { Addresses, BookList, CommentKind, Comments, Users } from 'database'; import { faker } from '@faker-js/faker'; -import { JsonSdkPromise } from 'json-api-nestjs-sdk'; +import { JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { creatSdk, run } from '../utils/run-application'; import { INestApplication } from '@nestjs/common'; diff --git a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts index b4e57aca..74698971 100644 --- a/apps/json-api-server-e2e/src/json-api/utils/run-application.ts +++ b/apps/json-api-server-e2e/src/json-api/utils/run-application.ts @@ -1,6 +1,6 @@ import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; -import { adapterForAxios, JsonApiJs } from 'json-api-nestjs-sdk'; +import { adapterForAxios, JsonApiJs } from '@klerick/json-api-nestjs-sdk'; import { RpcFactory, axiosTransportFactory, diff --git a/libs/json-api/json-api-nestjs-sdk/README.md b/libs/json-api/json-api-nestjs-sdk/README.md index 15f7fbc4..51cc5bf0 100644 --- a/libs/json-api/json-api-nestjs-sdk/README.md +++ b/libs/json-api/json-api-nestjs-sdk/README.md @@ -14,7 +14,7 @@ The plugin of client for help work with JSON API over [json-api-nestjs](https:// ## Installation ```bash $ -npm install json-api-nestjs-sdk +npm install @klerick/json-api-nestjs-sdk ``` ## Example @@ -27,7 +27,7 @@ import { FilterOperand, JsonApiJs, JsonSdkPromise, -} from 'json-api-nestjs-sdk'; +} from '@klerick/json-api-nestjs-sdk'; import { faker } from '@faker-js/faker'; import axios from 'axios'; @@ -93,7 +93,11 @@ const [addressPost, managerPost, rolesPost, userPost] = await jsonSdk ``` or you can use Angular module: ```typescript -import { provideJsonApi, AtomicFactory, JsonApiSdkService } from 'json-api-nestjs-sdk/ngModule'; +import { + provideJsonApi, + AtomicFactory, + JsonApiSdkService +} from '@klerick/json-api-nestjs-sdk/ngModule'; import { provideHttpClient, withFetch, diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts index ecc3a2b1..6a2d03de 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts @@ -1,4 +1,4 @@ -import { EntityField, EntityProps } from 'json-shared-type'; +import { EntityField, EntityProps } from '@klerick/json-api-nestjs-shared'; export { EntityField, EntityProps }; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts index ba47b6e3..6e161821 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts @@ -1,4 +1,4 @@ -import { FilterOperand } from 'json-shared-type'; +import { FilterOperand } from '@klerick/json-api-nestjs-shared'; export { FilterOperand }; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts index 33f67dab..fbd6a440 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts @@ -10,4 +10,4 @@ export { ResourceData, ResourceObject, ResourceObjectRelationships, -} from 'json-shared-type'; +} from '@klerick/json-api-nestjs-shared'; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts index 4ce83427..e6a8a2a4 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts @@ -1,4 +1,4 @@ -import { TypeOfArray } from 'json-shared-type'; +import { TypeOfArray } from '@klerick/json-api-nestjs-shared'; export { TypeOfArray }; type IntersectionToObj = { diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts index f768ed57..eec0812d 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts @@ -69,12 +69,14 @@ describe('GenerateAtomicBody', () => { text: entity.text, }, relationships: { - users: [ - { - id: `${user.id}`, - type: 'users', - }, - ], + users: { + data: [ + { + id: `${user.id}`, + type: 'users', + }, + ], + }, }, type: 'book-list', }, @@ -102,12 +104,14 @@ describe('GenerateAtomicBody', () => { text: entity.text, }, relationships: { - users: [ - { - id: `${user.id}`, - type: 'users', - }, - ], + users: { + data: [ + { + id: `${user.id}`, + type: 'users', + }, + ], + }, }, type: 'book-list', }, diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts index 1367c3c2..2436ccc3 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts @@ -1,4 +1,4 @@ -import { camelToKebab } from 'shared-utils'; +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; import { JsonApiSdkConfig, JsonSdkConfig } from '../types'; import { ID_KEY } from '../constants'; @@ -14,7 +14,7 @@ export { capitalizeFirstChar, kebabToCamel, isObject, -} from 'shared-utils'; +} from '@klerick/json-api-nestjs-shared'; export function resultConfig(partialConfig: JsonSdkConfig): JsonApiSdkConfig { return { diff --git a/nx.json b/nx.json index 4cd992f1..9a865fa3 100644 --- a/nx.json +++ b/nx.json @@ -89,6 +89,7 @@ "!type-for-rpc" ], "version": { + "preVersionCommand": "npx nx run-many -t build", "conventionalCommits": true, "generatorOptions": { "fallbackCurrentVersionResolver": "1.0.0" diff --git a/package-lock.json b/package-lock.json index 1f80a0a5..6087b0a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,10 @@ "@angular/platform-browser": "19.0.3", "@angular/platform-browser-dynamic": "19.0.3", "@angular/router": "19.0.3", + "@mikro-orm/core": "^6.4.3", + "@mikro-orm/mysql": "^6.4.3", + "@mikro-orm/nestjs": "^6.0.2", + "@mikro-orm/postgresql": "^6.4.3", "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", @@ -39,8 +43,8 @@ "tslib": "^2.3.0", "typeorm": "^0.3.20", "uuid": "^10.0.0", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0", "zone.js": "0.15.0" }, "devDependencies": { @@ -83,7 +87,7 @@ "eslint-config-prettier": "^9.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "jest-environment-node": "^29.4.1", + "jest-environment-node": "^29.7.0", "jest-preset-angular": "14.4.2", "jsonc-eslint-parser": "^2.1.0", "ng-packagr": "19.0.1", @@ -4469,6 +4473,188 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==" }, + "node_modules/@mikro-orm/core": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.3.tgz", + "integrity": "sha512-UTaqKs1bomYtGmEEZ8sNBOmW2OqT5NcMh+pBV2iJ6WLM5MuiIEuNhDMuvvPE5gNEwUzc1HyRhUV87bRDhDIGRg==", + "dependencies": { + "dataloader": "2.2.3", + "dotenv": "16.4.7", + "esprima": "4.0.1", + "fs-extra": "11.2.0", + "globby": "11.1.0", + "mikro-orm": "6.4.3", + "reflect-metadata": "0.2.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/b4nan" + } + }, + "node_modules/@mikro-orm/core/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@mikro-orm/core/node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/@mikro-orm/knex": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.3.tgz", + "integrity": "sha512-gVkRD/cIn6qxk/P9nR+IufZxJwuCCdv0AtcGvShxXXvaoIrQPJYDV7HRxBOHCEyNygr6M3Fqpph1oPoT6aezTQ==", + "dependencies": { + "fs-extra": "11.2.0", + "knex": "3.1.0", + "sqlstring": "2.3.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0", + "better-sqlite3": "*", + "libsql": "*", + "mariadb": "*" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "libsql": { + "optional": true + }, + "mariadb": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/knex/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@mikro-orm/mysql": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.3.tgz", + "integrity": "sha512-ZkrrzOWE9ouifU331q70K9BfAOD9SFRiNLNnECnzVrvDPWnthMV0ahGium9HyHpG4nev0Ybg6vnvq9IQzW1brg==", + "dependencies": { + "@mikro-orm/knex": "6.4.3", + "mysql2": "3.12.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "node_modules/@mikro-orm/nestjs": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@mikro-orm/nestjs/-/nestjs-6.0.2.tgz", + "integrity": "sha512-3bUBnJ0HwwIjsNPb0n7dfTWayqA5iSLHv/zM9juyBADSpJB2oBAd96QlUqDvNi/RckSg0m+ifLVT0Uw8e0+POg==", + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0 || ^6.0.0-dev.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" + } + }, + "node_modules/@mikro-orm/postgresql": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/postgresql/-/postgresql-6.4.3.tgz", + "integrity": "sha512-3cGi1gW6ME3SyuRRiJmSBtzHFa6Kavy6bK9rsSAAfXz+Pso6UBsqvesATbruKxDF7/CLdQlIY3CZZHXksUIrQg==", + "dependencies": { + "@mikro-orm/knex": "6.4.3", + "pg": "8.13.1", + "postgres-array": "3.0.2", + "postgres-date": "2.1.0", + "postgres-interval": "4.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/pg": { + "version": "8.13.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.13.1.tgz", + "integrity": "sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ==", + "dependencies": { + "pg-connection-string": "^2.7.0", + "pg-pool": "^3.7.0", + "pg-protocol": "^1.7.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/pg-connection-string": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" + }, + "node_modules/@mikro-orm/postgresql/node_modules/postgres-array": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", + "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/postgres-date": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@mikro-orm/postgresql/node_modules/postgres-interval": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-4.0.2.tgz", + "integrity": "sha512-EMsphSQ1YkQqKZL2cuG0zHkmjCCzQqQ71l2GXITqRwjhRleCdv00bDk/ktaSi0LnlaPzAc3535KTrjXsTdtx7A==", + "engines": { + "node": ">=12" + } + }, "node_modules/@module-federation/bridge-react-webpack-plugin": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.7.6.tgz", @@ -5554,7 +5740,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -5567,7 +5752,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -5576,7 +5760,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -10746,7 +10929,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, "engines": { "node": ">=8" } @@ -10843,6 +11025,14 @@ "node": "*" } }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aws4": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", @@ -11409,7 +11599,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -12923,6 +13112,11 @@ "node": ">=12" } }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==" + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -13062,6 +13256,14 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "dev": true }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -13149,7 +13351,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, "dependencies": { "path-type": "^4.0.0" }, @@ -13256,9 +13457,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "engines": { "node": ">=12" }, @@ -14010,6 +14211,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -14031,7 +14240,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -14337,7 +14545,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -14393,7 +14600,6 @@ "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -14471,7 +14677,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -14997,6 +15202,14 @@ "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -15048,7 +15261,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true, "engines": { "node": ">=8.0.0" } @@ -15065,6 +15277,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -15098,7 +15315,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -15195,7 +15411,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -15225,8 +15440,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -15788,7 +16002,6 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "dev": true, "engines": { "node": ">= 4" } @@ -15962,7 +16175,6 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -15989,7 +16201,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -16030,7 +16241,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -16096,7 +16306,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -16146,6 +16355,11 @@ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "dev": true }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -17566,7 +17780,6 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dev": true, "dependencies": { "universalify": "^2.0.0" }, @@ -17723,6 +17936,77 @@ "node": ">= 8" } }, + "node_modules/knex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/knex/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/knex/node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/koa": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", @@ -18475,6 +18759,11 @@ "node": ">=8.0" } }, + "node_modules/long": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==" + }, "node_modules/long-timeout": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/long-timeout/-/long-timeout-0.1.1.tgz", @@ -18528,6 +18817,20 @@ "yallist": "^3.0.2" } }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/luxon": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", @@ -18666,7 +18969,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "engines": { "node": ">= 8" } @@ -18683,7 +18985,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -18696,7 +18997,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -18704,6 +19004,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mikro-orm": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.3.tgz", + "integrity": "sha512-xDNzmLiL4EUTMOu9CbZ2d0sNIaUdH4RzDv4oqw27+u0/FPfvZTIagd+luxx1lWWqe/vg/iNtvqr5OcNQIYYrtQ==", + "engines": { + "node": ">= 18.12.0" + } + }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -19183,6 +19491,36 @@ "rimraf": "bin.js" } }, + "node_modules/mysql2": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.12.0.tgz", + "integrity": "sha512-C8fWhVysZoH63tJbX8d10IAoYCyXy4fdRFz2Ihrt9jtPILYynFEKUUzpp1U7qxzDc3tMbotvaBH+sl6bFnGZiw==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", @@ -19193,6 +19531,25 @@ "thenify-all": "^1.0.0" } }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/nanoclone": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", @@ -20681,8 +21038,7 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { "version": "1.11.1", @@ -20716,7 +21072,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, "engines": { "node": ">=8" } @@ -20851,17 +21206,17 @@ "dev": true }, "node_modules/pg-pool": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", - "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz", + "integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", - "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz", + "integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -21969,7 +22324,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -22137,7 +22491,6 @@ "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dev": true, "dependencies": { "resolve": "^1.20.0" }, @@ -22260,7 +22613,6 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -22302,7 +22654,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, "engines": { "node": ">=8" } @@ -22390,7 +22741,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -22470,7 +22820,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -22720,6 +23069,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -22947,7 +23301,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, "engines": { "node": ">=8" } @@ -23295,6 +23648,14 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", @@ -23682,7 +24043,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, "engines": { "node": ">= 0.4" }, @@ -23876,6 +24236,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/terser": { "version": "5.36.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", @@ -24099,6 +24467,14 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -24118,7 +24494,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -24851,7 +25226,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, "engines": { "node": ">= 10.0.0" } @@ -26748,17 +27122,17 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-validation-error": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.0.2.tgz", - "integrity": "sha512-21xGaDmnU7lJZ4J63n5GXWqi+rTzGy3gDHbuZ1jP6xrK/DEQGyOqs/xW7eH96tIfCOYm+ecCuT0bfajBRKEVUw==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-3.4.0.tgz", + "integrity": "sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==", "engines": { "node": ">=18.0.0" }, diff --git a/package.json b/package.json index 7bba1b85..2dc49ddf 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,10 @@ "@angular/platform-browser": "19.0.3", "@angular/platform-browser-dynamic": "19.0.3", "@angular/router": "19.0.3", + "@mikro-orm/core": "^6.4.3", + "@mikro-orm/mysql": "^6.4.3", + "@mikro-orm/nestjs": "^6.0.2", + "@mikro-orm/postgresql": "^6.4.3", "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", @@ -42,8 +46,8 @@ "tslib": "^2.3.0", "typeorm": "^0.3.20", "uuid": "^10.0.0", - "zod": "^3.22.4", - "zod-validation-error": "^3.0.2", + "zod": "^3.24.1", + "zod-validation-error": "^3.4.0", "zone.js": "0.15.0" }, "devDependencies": { @@ -86,7 +90,7 @@ "eslint-config-prettier": "^9.0.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", - "jest-environment-node": "^29.4.1", + "jest-environment-node": "^29.7.0", "jest-preset-angular": "14.4.2", "jsonc-eslint-parser": "^2.1.0", "ng-packagr": "19.0.1", diff --git a/tsconfig.base.json b/tsconfig.base.json index ac808542..06afef18 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,18 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@klerick/json-api-nestjs": [ + "libs/json-api/json-api-nestjs/src/index.ts" + ], + "@klerick/json-api-nestjs-sdk": [ + "libs/json-api/json-api-nestjs-sdk/src/index.ts" + ], + "@klerick/json-api-nestjs-sdk/ngModule": [ + "libs/json-api/json-api-nestjs-sdk/src/ngModule.ts" + ], + "@klerick/json-api-nestjs-shared": [ + "libs/json-api/json-api-nestjs-shared/src/index.ts" + ], "@klerick/nestjs-json-rpc": [ "libs/json-rpc/nestjs-json-rpc/src/index.ts" ], @@ -25,14 +37,7 @@ "libs/json-rpc/nestjs-json-rpc-sdk/src/ngModule.ts" ], "@nestjs-json-api/type-for-rpc": ["libs/type-for-rpc/src/index.ts"], - "database": ["libs/database/src/index.ts"], - "json-api-nestjs": ["libs/json-api/json-api-nestjs/src/index.ts"], - "json-api-nestjs-sdk": ["libs/json-api/json-api-nestjs-sdk/src/index.ts"], - "json-api-nestjs-sdk/ngModule": [ - "libs/json-api/json-api-nestjs-sdk/src/ngModule.ts" - ], - "json-shared-type": ["libs/json-api/json-shared-type/src/index.ts"], - "shared-utils": ["libs/shared-utils/src/index.ts"] + "database": ["libs/database/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] From 8935ade96d54b052f098e95dc1f600e26dbc7d50 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 07:02:10 +0100 Subject: [PATCH 09/26] refactor(json-api-nestjs): Rename npm package and fox data BREAKING CHANGE: Rename npm package from json-api-nestjs to @klerick/json-api-nestjs --- .../extend-book-list/extend-book-list.controller.ts | 2 +- .../controllers/extend-user/extend-user.controller.ts | 2 +- apps/json-api-server/src/app/resources/resources.module.ts | 3 ++- .../src/app/resources/service/example.pipe.ts | 2 +- .../src/app/resources/service/guard.service.ts | 2 +- libs/json-api/json-api-nestjs/tsconfig.json | 7 ++----- libs/json-api/json-api-nestjs/tsconfig.spec.json | 1 + 7 files changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts b/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts index 971bba19..612f74e3 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts +++ b/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts @@ -1,6 +1,6 @@ import { ParseUUIDPipe } from '@nestjs/common'; import { BookList } from 'database'; -import { JsonApi, JsonBaseController } from 'json-api-nestjs'; +import { JsonApi, JsonBaseController } from '@klerick/json-api-nestjs'; @JsonApi(BookList, { pipeForId: ParseUUIDPipe, diff --git a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts b/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts index 01acce2f..00c4ea1b 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts +++ b/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts @@ -19,7 +19,7 @@ import { PatchRelationshipData, ResourceObjectRelationships, PostData, -} from 'json-api-nestjs'; +} from '@klerick/json-api-nestjs'; import { ExamplePipe } from '../../service/example.pipe'; import { ExampleService } from '../../service/example.service'; import { ControllerInterceptor } from '../../service/controller.interceptor'; diff --git a/apps/json-api-server/src/app/resources/resources.module.ts b/apps/json-api-server/src/app/resources/resources.module.ts index 1c4d0e8d..6a6b5e2e 100644 --- a/apps/json-api-server/src/app/resources/resources.module.ts +++ b/apps/json-api-server/src/app/resources/resources.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { JsonApiModule } from 'json-api-nestjs'; +import { JsonApiModule, TypeOrmModule } from '@klerick/json-api-nestjs'; import { Users, Addresses, Comments, Roles, BookList } from 'database'; import { ExtendBookListController } from './controllers/extend-book-list/extend-book-list.controller'; import { ExtendUserController } from './controllers/extend-user/extend-user.controller'; @@ -11,6 +11,7 @@ import { ExampleService } from './service/example.service'; entities: [Users, Addresses, Comments, Roles, BookList], controllers: [ExtendBookListController, ExtendUserController], providers: [ExampleService], + type: TypeOrmModule, options: { debug: true, requiredSelectField: false, diff --git a/apps/json-api-server/src/app/resources/service/example.pipe.ts b/apps/json-api-server/src/app/resources/service/example.pipe.ts index 63d0dc9f..7cdf2399 100644 --- a/apps/json-api-server/src/app/resources/service/example.pipe.ts +++ b/apps/json-api-server/src/app/resources/service/example.pipe.ts @@ -4,7 +4,7 @@ import { PipeTransform, } from '@nestjs/common'; -import { Query } from 'json-api-nestjs'; +import { Query } from '@klerick/json-api-nestjs'; import { Users } from 'database'; export class ExamplePipe implements PipeTransform, Query> { diff --git a/apps/json-api-server/src/app/resources/service/guard.service.ts b/apps/json-api-server/src/app/resources/service/guard.service.ts index 66714d6e..a8567bc2 100644 --- a/apps/json-api-server/src/app/resources/service/guard.service.ts +++ b/apps/json-api-server/src/app/resources/service/guard.service.ts @@ -6,7 +6,7 @@ import { Injectable, } from '@nestjs/common'; import { Observable } from 'rxjs'; -import { entityForClass } from 'json-api-nestjs'; +import { entityForClass } from '@klerick/json-api-nestjs'; import { Request } from 'express'; import { Reflector } from '@nestjs/core'; diff --git a/libs/json-api/json-api-nestjs/tsconfig.json b/libs/json-api/json-api-nestjs/tsconfig.json index f22982be..0dc79caa 100644 --- a/libs/json-api/json-api-nestjs/tsconfig.json +++ b/libs/json-api/json-api-nestjs/tsconfig.json @@ -5,9 +5,9 @@ "forceConsistentCasingInFileNames": true, "strict": true, "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true }, "files": [], "include": [], @@ -15,9 +15,6 @@ { "path": "./tsconfig.lib.json" }, - { - "path": "./tsconfig-mjs.lib.json" - }, { "path": "./tsconfig.spec.json" } diff --git a/libs/json-api/json-api-nestjs/tsconfig.spec.json b/libs/json-api/json-api-nestjs/tsconfig.spec.json index 69a251f3..ebbb8e0d 100644 --- a/libs/json-api/json-api-nestjs/tsconfig.spec.json +++ b/libs/json-api/json-api-nestjs/tsconfig.spec.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "../../../dist/out-tsc", "module": "commonjs", + "moduleResolution": "node16", "types": ["jest", "node"] }, "include": [ From ab025bf17e68ad2bef14fa16c3d5854a6189ad05 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 07:05:28 +0100 Subject: [PATCH 10/26] chore: Change README.md Closes: #89 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 219146a1..2893a528 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ $ npm run seed:run ```bash # dev server -$ npm run demo:json-api +$ nx run json-api-server:serve:development ``` ## License From f41fe6aaccab87c4438b62388185869e631ba43b Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 07:39:49 +0100 Subject: [PATCH 11/26] test(json-api-nestjs-sdk): Fix tests --- .../service/json-api-utils.service.spec.ts | 20 +++++++++++-------- .../lib/utils/generate-atomic-body.spec.ts | 14 +++++++------ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.spec.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.spec.ts index 95feb008..dd04a10e 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.spec.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.spec.ts @@ -453,15 +453,19 @@ describe('JsonApiUtilsService', () => { }, relationships: { relatedEntity: { - type: 'related-entity', - id: '2', - }, - relatedEntities: [ - { - type: 'related-entities', - id: '3', + data: { + type: 'related-entity', + id: '2', }, - ], + }, + relatedEntities: { + data: [ + { + type: 'related-entities', + id: '3', + }, + ], + }, }, }); }); diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts index eec0812d..4a278865 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/generate-atomic-body.spec.ts @@ -151,12 +151,14 @@ describe('GenerateAtomicBody', () => { text: entity.text, }, relationships: { - users: [ - { - id: `${user.id}`, - type: 'users', - }, - ], + users: { + data: [ + { + id: `${user.id}`, + type: 'users', + }, + ], + }, }, type: 'book-list', }, From 30b398804f241e34b976481d28a132efd8def4f5 Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 22 Jan 2025 19:47:32 +0100 Subject: [PATCH 12/26] refactor(json-api-nestjs): some refactoring swagger generator --- .env | 3 +- .../src/lib/service/json-api-utils.service.ts | 5 +- .../src/lib/utils/object-utils.ts | 7 + .../src/lib/modules/mixin/mixin.module.ts | 2 + .../mixin/swagger/filter-operand-model.ts | 84 +++++ .../src/lib/modules/mixin/swagger/index.ts | 1 + .../mixin/swagger/method/delete-one.ts | 46 +++ .../swagger/method/delete-relationship.ts | 78 +++++ .../modules/mixin/swagger/method/get-all.ts | 226 +++++++++++++ .../modules/mixin/swagger/method/get-one.ts | 108 ++++++ .../mixin/swagger/method/get-relationship.ts | 64 ++++ .../lib/modules/mixin/swagger/method/index.ts | 28 ++ .../modules/mixin/swagger/method/patch-one.ts | 79 +++++ .../swagger/method/patch-relationship.ts | 79 +++++ .../modules/mixin/swagger/method/post-one.ts | 71 ++++ .../mixin/swagger/method/post-relationship.ts | 79 +++++ .../mixin/swagger/swagger-bind.service.ts | 103 ++++++ .../src/lib/modules/mixin/swagger/utils.ts | 312 ++++++++++++++++++ .../src/lib/modules/mixin/types/zod-types.ts | 2 + .../zod/zod-input-patch-schema/index.spec.ts | 6 + .../zod/zod-input-post-schema/index.spec.ts | 6 + .../mixin/zod/zod-share/attributes.spec.ts | 6 + .../lib/modules/type-orm/orm-helper/index.ts | 8 +- package-lock.json | 28 +- package.json | 1 - 25 files changed, 1404 insertions(+), 28 deletions(-) create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts diff --git a/.env b/.env index eaa22638..6aa8c6ae 100644 --- a/.env +++ b/.env @@ -3,7 +3,8 @@ DB_LOGGING=1 DB_USERNAME="postgres" DB_PASSWORD="postgres" -DB_NAME="json-api-db" +#DB_NAME="json-api-db" +DB_NAME="postgres" DB_PORT=5432 DB_TYPE=postgres diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts index b85cd65f..68dc6f0e 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts @@ -1,3 +1,5 @@ +import { createEntityInstance } from '@klerick/json-api-nestjs-shared'; + import { Attributes, Entity as EntityObject, @@ -260,8 +262,7 @@ export class JsonApiUtilsService { } createEntityInstance(name: string): E { - const entityName = kebabToCamel(name); - return Function('return new class ' + entityName + '{}')(); + return createEntityInstance(name); } private findIncludeEntity>>( diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts b/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts index 461455b3..5d4ad26d 100644 --- a/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts +++ b/libs/json-api/json-api-nestjs-shared/src/lib/utils/object-utils.ts @@ -1,3 +1,5 @@ +import { kebabToCamel } from './string-utils'; + export const ObjectTyped = { keys: Object.keys as (yourObject: T) => Array, values: Object.values as (yourObject: U) => Array, @@ -12,3 +14,8 @@ export const ObjectTyped = { export function isObject(item: unknown): item is object { return typeof item === 'object' && !Array.isArray(item) && item !== null; } + +export function createEntityInstance(name: string): E { + const entityName = kebabToCamel(name); + return Function('return new class ' + entityName + '{}')(); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts index 7d4ea222..5dc4e8f1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts @@ -22,6 +22,7 @@ import { ZodInputPatchRelationshipSchema, ZodInputPostRelationshipSchema, } from './factory'; +import { SwaggerBindService } from './swagger'; export class MixinModule { static forRoot(options: MixinOptions): DynamicModule { @@ -75,6 +76,7 @@ export class MixinModule { ZodQuerySchema(), ZodPatchSchema(), ZodPostSchema(), + SwaggerBindService, ZodInputPatchRelationshipSchema, ZodInputPostRelationshipSchema, ], diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts new file mode 100644 index 00000000..a71b36ee --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts @@ -0,0 +1,84 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { FilterOperand as FilterOperandType } from '@klerick/json-api-nestjs-shared'; + +const title = 'is equal to the conditional of query'; + +export const OperandsMapTitle = { + [FilterOperandType.in]: `${title} "WHERE 'attribute_name' IN ('value1', 'value2')"`, + [FilterOperandType.nin]: `${title} "WHERE 'attribute_name' NOT IN ('value1', 'value1')"`, + [FilterOperandType.eq]: `${title} "WHERE 'attribute_name' = 'value1'`, + [FilterOperandType.ne]: `${title} "WHERE 'attribute_name' <> 'value1'`, + [FilterOperandType.gt]: `${title} "WHERE 'attribute_name' > 'value1'`, + [FilterOperandType.gte]: `${title} "WHERE 'attribute_name' >= 'value1'`, + [FilterOperandType.like]: `${title} "WHERE 'attribute_name' ILIKE %value1%`, + [FilterOperandType.lt]: `${title} "WHERE 'attribute_name' < 'value1'`, + [FilterOperandType.lte]: `${title} "WHERE 'attribute_name' <= 'value1'`, + [FilterOperandType.regexp]: `${title} "WHERE 'attribute_name' ~* value1`, + [FilterOperandType.some]: `${title} "WHERE 'attribute_name' && [value1]`, +}; + +export class FilterOperand { + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.in], + required: false, + type: 'array', + items: { + type: 'string', + }, + }) + [FilterOperandType.in]!: string[]; + + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.nin], + required: false, + type: 'array', + items: { + type: 'string', + }, + }) + [FilterOperandType.nin]!: string[]; + + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.eq], + required: false, + }) + [FilterOperandType.eq]!: string; + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.ne], + required: false, + }) + [FilterOperandType.ne]!: string; + + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.gte], + required: false, + }) + [FilterOperandType.gte]!: string; + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.gt], + required: false, + }) + [FilterOperandType.gt]!: string; + + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.lt], + required: false, + }) + [FilterOperandType.lt]!: string; + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.lte], + required: false, + }) + [FilterOperandType.lte]!: string; + + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.regexp], + required: false, + }) + [FilterOperandType.regexp]!: string; + @ApiProperty({ + title: OperandsMapTitle[FilterOperandType.some], + required: false, + }) + [FilterOperandType.some]!: string; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/index.ts new file mode 100644 index 00000000..fc7c3daa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/index.ts @@ -0,0 +1 @@ +export * from './swagger-bind.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts new file mode 100644 index 00000000..092f8f85 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts @@ -0,0 +1,46 @@ +import { EntityClass, EntityTarget, ObjectLiteral } from '../../../../types'; +import { Type } from '@nestjs/common'; +import { EntityProps, TypeField, ZodParams } from '../../types'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { errorSchema } from '../utils'; + +export function deleteOne( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + zodParams: ZodParams, string>, + methodName: string +) { + const entityName = entity.name; + + const { typeId } = zodParams; + + ApiParam({ + name: 'id', + required: true, + type: typeId === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiOperation({ + summary: `Delete item of resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 404, + description: `Item of resource "${entityName}" not found`, + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 204, + description: `Item of resource "${entityName}" has been deleted`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong query parameters', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts new file mode 100644 index 00000000..b78b2c38 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts @@ -0,0 +1,78 @@ +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { Type } from '@nestjs/common'; + +import { EntityProps, TypeField, ZodParams } from '../../types'; +import { zodPatchRelationship } from '../../zod'; +import { errorSchema } from '../utils'; +import { generateSchema } from '@anatine/zod-openapi'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; + +export function deleteRelationship( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + zodParams: ZodParams, string>, + methodName: string +) { + const entityName = entity.name; + + const { + entityFieldsStructure: { relations }, + typeId, + } = zodParams; + + ApiOperation({ + summary: `Delete list of relation for resource "${entityName}"`, + operationId: `${controller.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: typeId === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'relName', + required: true, + type: 'string', + enum: relations, + description: `Relation name of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiBody({ + description: `Json api schema for delete "${entityName}" item`, + schema: generateSchema(zodPatchRelationship) as + | SchemaObject + | ReferenceObject, + required: true, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong url parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Incorrect type for relation', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 404, + description: 'Resource not found ', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 204, + description: `Item/s of relation for "${entityName}" has been deleted`, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts new file mode 100644 index 00000000..a5ed5e39 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts @@ -0,0 +1,226 @@ +import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { Type } from '@nestjs/common'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { EntityProps, ZodParams } from '../../types'; +import { errorSchema, jsonSchemaResponse } from '../utils'; +import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../../constants'; + +export function getAll( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + zodParams: ZodParams, string>, + methodName: string +): void { + const { entityFieldsStructure, entityRelationStructure, primaryColumn } = + zodParams; + const { field, relations } = entityFieldsStructure; + + const relationTree = ObjectTyped.entries(entityRelationStructure).reduce( + (acum, [name, filed]) => { + acum.push(...filed.map((i) => `${name.toLocaleString()}.${i}`)); + return acum; + }, + [] as string[] + ); + + ApiOperation({ + summary: `Get list items of resource "${entity.name}"`, + operationId: `${controller.constructor.name}_${methodName}`, + servers: undefined, + })(controller, methodName, descriptor); + + ApiQuery({ + name: 'fields', + required: false, + style: 'deepObject', + schema: { + type: 'object', + }, + examples: { + allField: { + summary: 'Select all field', + description: 'Select field for target and relation', + value: { + target: field.join(','), + ...ObjectTyped.entries(entityRelationStructure).reduce( + (acum, [name, props]) => { + acum[name.toString()] = props.join(','); + return acum; + }, + {} as Record + ), + }, + }, + selectOnlyIdsTarget: { + summary: 'Select ids for target', + description: 'Select ids for target', + value: { + target: field.filter((i) => i === primaryColumn).join(','), + }, + }, + }, + description: `Object of field for select field from "${entity.name}" resource`, + })(controller, methodName, descriptor); + + ApiQuery({ + name: 'filter', + required: false, + style: 'deepObject', + schema: { + type: 'object', + }, + examples: { + simpleExample: { + summary: 'Several conditional', + description: 'Get if relation is not null', + value: { + [field[0]]: { + in: '1,2,3', + }, + [field[1]]: { + lt: '1', + }, + [relationTree[0]]: { + eq: 'test', + }, + }, + }, + relationNull: { + summary: 'Get if relation is null', + description: 'Get if relation is null', + value: { + [relations[0]]: { + eq: null, + }, + }, + }, + relationNotNull: { + summary: 'Get if relation is not null', + description: 'Get if relation is not null', + value: { + [relations[0]]: { + ne: null, + }, + }, + }, + getRelationByConditional: { + summary: 'Get if relation field is', + description: 'Get if relation field is', + value: { + [relationTree[0]]: { + eq: 'test', + }, + }, + }, + }, + description: `Object of filter for select items from "${entity.name}" resource`, + })(controller, methodName, descriptor); + + let sortAscRelation = {}; + let sortDescRelation = {}; + let sortSeveral = { + summary: 'Sort several field', + description: 'Sort several field', + value: `${field[1]},-${field[0]}`, + }; + + if (relations.length > 0) { + ApiQuery({ + name: 'include', + required: false, + enum: relations, + style: 'simple', + isArray: true, + description: `"${entity.name}" resource item has been extended with existing relations`, + examples: { + withInclude: { + summary: 'Add all relation', + description: 'Add all realtion', + value: relations, + }, + without: { + summary: 'Without relation', + description: 'Without all relation', + value: [], + }, + }, + })(controller, methodName, descriptor); + + sortAscRelation = { + summary: 'Sort field relation by ASC', + description: 'Sort field relation by ASC', + value: relationTree[2], + }; + sortDescRelation = { + summary: 'Sort field relation by DESC', + description: 'Sort field relation by DESC', + value: `-${relationTree[2]}`, + }; + sortSeveral = { + summary: 'Sort several field with relation', + description: 'Sort several field relation', + value: `${field[1]},-${relationTree[2]},${relationTree[1]},-${field[0]}`, + }; + } + + ApiQuery({ + name: 'sort', + type: 'string', + required: false, + description: `Params for sorting of "${entity.name}"`, + examples: { + sortAsc: { + summary: 'Sort field by ASC', + description: 'Sort field by ASC', + value: field[1], + }, + sortDesc: { + summary: 'Sort field by DESC', + description: 'Sort field by DESC', + value: `-${field[1]}`, + }, + sortAscRelation, + sortDescRelation, + sortSeveral, + }, + })(controller, methodName, descriptor); + + ApiQuery({ + name: 'page', + style: 'deepObject', + required: false, + schema: { + type: 'object', + properties: { + number: { + type: 'integer', + minimum: 1, + example: DEFAULT_QUERY_PAGE, + }, + size: { + type: 'integer', + minimum: 1, + example: DEFAULT_PAGE_SIZE, + maximum: 500, + }, + }, + additionalProperties: false, + }, + description: `"${entity.name}" resource has been limit and offset with this params.`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong query parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + description: 'Resource list received successfully', + schema: jsonSchemaResponse(entity, zodParams, true), + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts new file mode 100644 index 00000000..f28d93ae --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts @@ -0,0 +1,108 @@ +import { EntityClass, EntityTarget, ObjectLiteral } from '../../../../types'; +import { Type } from '@nestjs/common'; +import { EntityProps, TypeField, ZodParams } from '../../types'; +import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; +import { errorSchema, jsonSchemaResponse } from '../utils'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +export function getOne( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + zodParams: ZodParams, string>, + methodName: string +) { + const entityName = entity.name; + const { + entityFieldsStructure, + entityRelationStructure, + primaryColumn, + typeId, + } = zodParams; + const { field, relations } = entityFieldsStructure; + + ApiOperation({ + summary: `Get one item of resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: typeId === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiQuery({ + name: 'fields', + required: false, + style: 'deepObject', + schema: { + type: 'object', + }, + examples: { + allField: { + summary: 'Select all field', + description: 'Select field for target and relation', + value: { + target: field.join(','), + ...ObjectTyped.entries(entityRelationStructure).reduce( + (acum, [name, props]) => { + acum[name.toString()] = props.join(','); + return acum; + }, + {} as Record + ), + }, + }, + selectOnlyIdsTarget: { + summary: 'Select ids for target', + description: 'Select ids for target', + value: { + target: field.filter((i) => i === primaryColumn).join(','), + }, + }, + }, + description: `Object of field for select field from "${entity.name}" resource`, + })(controller, methodName, descriptor); + + if (relations.length > 0) { + ApiQuery({ + name: 'include', + required: false, + enum: relations, + style: 'simple', + isArray: true, + description: `"${entity.name}" resource item has been extended with existing relations`, + examples: { + withInclude: { + summary: 'Add all relation', + description: 'Add all realtion', + value: relations, + }, + without: { + summary: 'Without relation', + description: 'Without all relation', + value: [], + }, + }, + })(controller, methodName, descriptor); + } + ApiResponse({ + status: 404, + description: `Item of resource "${entityName}" not found`, + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong query parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + description: 'Resource one item received successfully', + schema: jsonSchemaResponse(entity, zodParams), + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts new file mode 100644 index 00000000..1396062f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts @@ -0,0 +1,64 @@ +import { EntityClass, EntityTarget, ObjectLiteral } from '../../../../types'; +import { Type } from '@nestjs/common'; +import { EntityProps, TypeField, ZodParams } from '../../types'; +import { errorSchema, schemaTypeForRelation } from '../utils'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; + +export function getRelationship( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + zodParams: ZodParams, string>, + methodName: string +) { + const entityName = entity.name; + + const { + entityFieldsStructure: { relations }, + typeId, + } = zodParams; + + ApiOperation({ + summary: `Get list of relation for resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: typeId === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'relName', + required: true, + type: 'string', + enum: relations, + description: `Relation name of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + schema: schemaTypeForRelation, + description: `Item/s of relation for "${entityName}" has been created`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong url parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Incorrect type for relation', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 404, + description: 'Resource not found ', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/index.ts new file mode 100644 index 00000000..61a4005f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/index.ts @@ -0,0 +1,28 @@ +import { getAll } from './get-all'; +import { getOne } from './get-one'; +import { deleteOne } from './delete-one'; +import { postOne } from './post-one'; +import { patchOne } from './patch-one'; +import { getRelationship } from './get-relationship'; +import { deleteRelationship } from './delete-relationship'; +import { postRelationship } from './post-relationship'; +import { patchRelationship } from './patch-relationship'; + +import { OrmService } from '../../types'; +import { ObjectLiteral } from '../../../../types'; + +export const swaggerMethod = { + getAll, + getOne, + deleteOne, + postOne, + patchOne, + getRelationship, + deleteRelationship, + postRelationship, + patchRelationship, +} as const; + +export type SwaggerMethod = { + [Key in keyof OrmService]?: (typeof swaggerMethod)[Key]; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts new file mode 100644 index 00000000..37a96d54 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts @@ -0,0 +1,79 @@ +import { Type } from '@nestjs/common'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { generateSchema } from '@anatine/zod-openapi'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { EntityProps, TypeField, ZodParams } from '../../types'; +import { errorSchema, jsonSchemaResponse } from '../utils'; +import { zodPatch } from '../../zod'; + +export function patchOne( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + zodParams: ZodParams, string>, + methodName: string +) { + const entityName = entity.name; + const { + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType, + } = zodParams; + + ApiOperation({ + summary: `Update item of resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: typeId === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiBody({ + description: `Json api schema for update "${entityName}" item`, + schema: generateSchema( + zodPatch( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ) + ) as SchemaObject | ReferenceObject, + required: true, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + description: `Item of resource "${entityName}" has been updated`, + schema: jsonSchemaResponse(entity, zodParams), + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong body parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Unprocessable data', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts new file mode 100644 index 00000000..55e8e3c5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts @@ -0,0 +1,79 @@ +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { generateSchema } from '@anatine/zod-openapi'; +import { Type } from '@nestjs/common'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { EntityProps, TypeField, ZodParams } from '../../types'; +import { errorSchema, schemaTypeForRelation } from '../utils'; +import { zodPatchRelationship } from '../../zod'; + +export function patchRelationship( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + zodParams: ZodParams, string>, + methodName: string +) { + const entityName = entity.name; + + const { + entityFieldsStructure: { relations }, + typeId, + } = zodParams; + + ApiOperation({ + summary: `Update list of relation for resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller.prototype, methodName, descriptor); + + ApiParam({ + name: 'id', + required: true, + type: typeId === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'relName', + required: true, + type: 'string', + enum: relations, + description: `Relation name of resource "${entityName}"`, + })(controller.prototype, methodName, descriptor); + + ApiBody({ + description: `Json api schema for update "${entityName}" item`, + schema: generateSchema(zodPatchRelationship) as + | SchemaObject + | ReferenceObject, + required: true, + })(controller.prototype, methodName, descriptor); + + ApiResponse({ + status: 200, + schema: schemaTypeForRelation, + description: `Item/s of relation for "${entityName}" has been updated`, + })(controller.prototype, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong url parameters', + schema: errorSchema, + })(controller.prototype, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Incorrect type for relation', + schema: errorSchema, + })(controller.prototype, methodName, descriptor); + + ApiResponse({ + status: 404, + description: 'Resource not found ', + schema: errorSchema, + })(controller.prototype, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts new file mode 100644 index 00000000..dab98f8a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts @@ -0,0 +1,71 @@ +import { ApiBody, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { generateSchema } from '@anatine/zod-openapi'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { Type } from '@nestjs/common'; + +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { EntityProps, ZodParams } from '../../types'; +import { errorSchema, jsonSchemaResponse } from '../utils'; +import { zodPost } from '../../zod'; + +export function postOne( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + zodParams: ZodParams, string>, + methodName: string +) { + const entityName = entity.name; + const { + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType, + } = zodParams; + ApiOperation({ + summary: `Create item of resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiBody({ + description: `Json api schema for new "${entityName}" item`, + schema: generateSchema( + zodPost( + typeId, + typeName, + fieldWithType, + propsDb, + primaryColumn, + relationArrayProps, + relationPopsName, + primaryColumnType + ) + ) as SchemaObject | ReferenceObject, + required: true, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 201, + description: `Item of resource "${entityName}" has been created`, + schema: jsonSchemaResponse(entity, zodParams), + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong body parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Unprocessable data', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts new file mode 100644 index 00000000..04f5f82e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts @@ -0,0 +1,79 @@ +import { Type } from '@nestjs/common'; +import { generateSchema } from '@anatine/zod-openapi'; +import { + ReferenceObject, + SchemaObject, +} from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; + +import { errorSchema, schemaTypeForRelation } from '../utils'; +import { zodPatchRelationship } from '../../zod'; +import { EntityProps, TypeField, ZodParams } from '../../types'; +import { EntityClass, ObjectLiteral } from '../../../../types'; + +export function postRelationship( + controller: Type, + descriptor: PropertyDescriptor, + entity: EntityClass, + zodParams: ZodParams, string>, + methodName: string +) { + const entityName = entity.name; + + const { + entityFieldsStructure: { relations }, + typeId, + } = zodParams; + + ApiParam({ + name: 'id', + required: true, + type: typeId === TypeField.number ? 'integer' : 'string', + description: `ID of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiParam({ + name: 'relName', + required: true, + type: 'string', + enum: relations, + description: `Relation name of resource "${entityName}"`, + })(controller, methodName, descriptor); + + ApiBody({ + description: `Json api schema for update "${entityName}" item`, + schema: generateSchema(zodPatchRelationship) as + | SchemaObject + | ReferenceObject, + required: true, + })(controller, methodName, descriptor); + + ApiOperation({ + summary: `Create list of relation for resource "${entityName}"`, + operationId: `${controller.constructor.name}_${methodName}`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 200, + schema: schemaTypeForRelation, + description: `Item/s of relation for "${entityName}" has been created`, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 400, + description: 'Wrong url parameters', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 422, + description: 'Incorrect type for relation', + schema: errorSchema, + })(controller, methodName, descriptor); + + ApiResponse({ + status: 404, + description: 'Resource not found ', + schema: errorSchema, + })(controller, methodName, descriptor); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts new file mode 100644 index 00000000..5e9a49c0 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts @@ -0,0 +1,103 @@ +import { Injectable, OnModuleInit, Inject } from '@nestjs/common'; +import { DECORATORS } from '@nestjs/swagger/dist/constants'; +import { ApiExtraModels, ApiTags } from '@nestjs/swagger'; +import { DiscoveryService } from '@nestjs/core'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { PARAMTYPES_METADATA } from '@nestjs/common/constants'; + +import { + CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, + JSON_API_CONTROLLER_POSTFIX, + JSON_API_DECORATOR_ENTITY, + PARAMS_FOR_ZOD_SCHEMA, +} from '../../../constants'; +import { getProviderName, nameIt } from '../helper'; +import { JsonBaseController } from '../controller/json-base.controller'; +import { EntityClass, ObjectLiteral } from '../../../types'; +import { DecoratorOptions, EntityProps, ZodParams } from '../types'; +import { FilterOperand } from './filter-operand-model'; +import { createApiModels } from './utils'; +import { Bindings } from '../config/bindings'; + +import { swaggerMethod } from './method'; + +@Injectable() +export class SwaggerBindService + implements OnModuleInit +{ + @Inject(CURRENT_ENTITY) private entity!: EntityClass; + @Inject(DiscoveryService) private discoveryService!: DiscoveryService; + @Inject(CONTROL_OPTIONS_TOKEN) private config!: DecoratorOptions; + @Inject(PARAMS_FOR_ZOD_SCHEMA) private zodParams!: ZodParams< + E, + EntityProps, + string + >; + + onModuleInit(): any { + this.initSwagger(); + } + + public initSwagger() { + const controllerName = nameIt( + getProviderName(this.entity.name, JSON_API_CONTROLLER_POSTFIX), + JsonBaseController + ).name; + + const controllerInst = this.discoveryService + .getControllers() + .find( + (i) => + i.name === controllerName || + this.entity === + Reflect.getMetadata( + JSON_API_DECORATOR_ENTITY, + i.instance.constructor + ) + ); + if (!controllerInst) + throw new Error(`Controller for ${this.entity.name} is empty`); + const controller = controllerInst.instance.constructor; + const apiTag = Reflect.getMetadata(DECORATORS.API_TAGS, controller); + if (!apiTag) { + ApiTags(this.config['overrideRoute'] || this.entity.name)(controller); + } + + ApiTags(this.entity.name)(controller); + + ApiExtraModels(FilterOperand)(controller); + ApiExtraModels(createApiModels(this.entity, this.zodParams))(controller); + + const { allowMethod = ObjectTyped.keys(Bindings) } = this.config; + for (const method of ObjectTyped.keys(Bindings)) { + if (!allowMethod.includes(method)) continue; + + if (!(method in swaggerMethod)) continue; + + const descriptor = Reflect.getOwnPropertyDescriptor( + controller.prototype, + method + ); + if (!descriptor) + throw new Error( + `Descriptor for entity controller ${this.entity.name} is empty` + ); + + swaggerMethod[method]( + controller.prototype, + descriptor, + this.entity, + this.zodParams, + method + ); + + Reflect.defineMetadata( + PARAMTYPES_METADATA, + [Object], + controller.prototype, + method + ); + } + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts new file mode 100644 index 00000000..4b1b284d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts @@ -0,0 +1,312 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { + ObjectTyped, + EntityRelation, + camelToKebab, +} from '@klerick/json-api-nestjs-shared'; + +import { EntityProps, TypeField, ZodParams } from '../types'; +import { EntityClass, ObjectLiteral } from '../../../types'; + +export const errorSchema = { + type: 'object', + properties: { + statusCode: { + type: 'number', + }, + error: { + type: 'string', + }, + message: { + type: 'array', + items: { + type: 'object', + properties: { + code: { + type: 'string', + }, + message: { + type: 'string', + }, + path: { + type: 'array', + items: { + type: 'string', + }, + }, + keys: { + type: 'array', + items: { + type: 'string', + }, + }, + }, + required: ['code', 'message', 'path'], + }, + }, + }, +}; + +export function jsonSchemaResponse( + entity: EntityClass, + zodParams: ZodParams, string>, + array = false +) { + const { + entityFieldsStructure, + fieldWithType, + relationArrayProps, + relationPopsName, + primaryColumn, + } = zodParams; + const { relations } = entityFieldsStructure; + + const relationTypeName = relationPopsName; + + const dataType = { + type: 'object', + properties: { + type: { + type: 'string', + const: camelToKebab(entity.name), + }, + id: { + type: 'string', + }, + attributes: { + type: 'object', + properties: ObjectTyped.entries(fieldWithType) + .filter(([name]) => name !== primaryColumn) + .reduce((acum, [name, type]) => { + switch (type) { + case TypeField.array: + acum[name.toString()] = { + type: 'array', + items: { + type: 'string', + }, + }; + break; + case TypeField.date: + acum[name.toString()] = { + format: 'date-time', + type: 'string', + }; + break; + case TypeField.number: + acum[name.toString()] = { + type: 'integer', + }; + break; + case TypeField.boolean: + acum[name.toString()] = { + type: 'boolean', + }; + break; + default: + acum[name.toString()] = { + type: 'string', + }; + } + return acum; + }, {} as Record), + }, + relationships: { + type: 'object', + properties: relations.reduce((acum, name) => { + const dataItem = { + type: 'object', + properties: { + type: { + type: 'string', + const: camelToKebab( + relationTypeName[name as EntityRelation] + ), + }, + id: { + type: 'string', + }, + }, + required: ['type', 'id'], + }; + const dataArray = { + type: 'array', + items: dataItem, + }; + acum[name.toString()] = { + type: 'object', + properties: { + links: { + type: 'object', + properties: { + self: { + type: 'string', + }, + }, + required: ['self'], + }, + data: relationArrayProps[name as EntityRelation] + ? dataArray + : dataItem, + }, + required: ['links'], + }; + return acum; + }, {} as Record), + }, + links: { + type: 'object', + properties: { + self: { + type: 'string', + }, + }, + required: ['self'], + }, + }, + }; + const dataTypeArra = { + type: 'array', + items: dataType, + }; + return { + type: 'object', + properties: { + meta: { + type: 'object', + }, + data: array ? dataTypeArra : dataType, + includes: { + type: 'array', + items: { + type: 'object', + properties: { + type: { + type: 'string', + }, + id: { + type: 'string', + }, + attributes: { + type: 'object', + }, + relationships: { + type: 'object', + properties: { + relationName: { + properties: { + links: { + type: 'object', + properties: { + self: { + type: 'string', + }, + }, + required: ['self'], + }, + }, + required: ['links'], + }, + }, + }, + links: { + type: 'object', + properties: { + self: { + type: 'string', + }, + }, + required: ['self'], + }, + }, + required: ['type', 'id', 'attributes'], + }, + }, + }, + required: ['meta', 'data'], + }; +} + +export function createApiModels( + entity: EntityClass, + zodParams: ZodParams, string> +): EntityClass { + const { + entityFieldsStructure, + propsType, + relationPopsName, + propsDb, + relationArrayProps, + } = zodParams; + + for (const [name, type] of ObjectTyped.entries(propsType)) { + const { field, relations } = entityFieldsStructure; + let currentType: any; + let required = false; + let isArray = false; + if (field.includes(name as string)) { + required = !propsDb[name].isNullable; + isArray = propsDb[name].isArray; + switch (propsType[name as EntityProps]) { + case TypeField.date: + currentType = Date; + break; + case TypeField.number: + currentType = Number; + break; + case TypeField.boolean: + currentType = Boolean; + break; + default: + currentType = String; + } + } + + if (relations.includes(name as string)) { + currentType = relationPopsName[name as EntityRelation]; + if (propsDb[name]) { + required = !propsDb[name].isNullable; + isArray = propsDb[name].isArray; + } else { + isArray = relationArrayProps[name as EntityRelation]; + required = !isArray; + } + } + + ApiProperty({ + required, + isArray, + type: () => currentType, + })(entity.prototype, name.toString()); + } + + return entity; +} + +const dataType = { + type: 'object', + properties: { + type: { + type: 'string', + }, + id: { + type: 'string', + }, + }, +}; +export const schemaTypeForRelation = { + type: 'object', + properties: { + data: { + oneOf: [ + dataType, + { type: 'null' }, + { + type: 'array', + items: dataType, + }, + ], + }, + }, +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts index cf413985..23072f39 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts @@ -114,6 +114,8 @@ export type AllFieldWithType = FieldWithType & { export type PropsForField = { [K in EntityProps]: PropsFieldItem; +} & { + [K in EntityRelation]: PropsFieldItem; }; export type ColumnType = diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts index b86972dd..6e15f6d9 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts @@ -37,6 +37,12 @@ const propsDb: PropsForField = { createdAt: { type: 'date', isArray: false, isNullable: true }, testDate: { type: 'date', isArray: false, isNullable: true }, updatedAt: { type: 'date', isArray: false, isNullable: true }, + notes: { type: 'string', isArray: false, isNullable: true }, + roles: { type: 'number', isArray: true, isNullable: true }, + addresses: { type: 'number', isArray: true, isNullable: true }, + userGroup: { type: 'number', isArray: false, isNullable: true }, + manager: { type: 'number', isArray: false, isNullable: true }, + comments: { type: 'number', isArray: true, isNullable: true }, }; const primaryColumn: EntityProps = 'id'; const relationArrayProps: RelationPropsArray = { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts index 61bb555b..b43feb78 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts @@ -37,6 +37,12 @@ const propsDb: PropsForField = { createdAt: { type: 'date', isArray: false, isNullable: true }, testDate: { type: 'date', isArray: false, isNullable: true }, updatedAt: { type: 'date', isArray: false, isNullable: true }, + notes: { type: 'string', isArray: false, isNullable: true }, + roles: { type: 'number', isArray: true, isNullable: true }, + addresses: { type: 'number', isArray: true, isNullable: true }, + userGroup: { type: 'number', isArray: false, isNullable: true }, + manager: { type: 'number', isArray: false, isNullable: true }, + comments: { type: 'number', isArray: true, isNullable: true }, }; const primaryColumn: EntityProps = 'id'; const relationArrayProps: RelationPropsArray = { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts index b9b7d3a9..dd94a577 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts @@ -26,6 +26,12 @@ const propsDb: PropsForField = { createdAt: { type: 'timestamp', isArray: false, isNullable: true }, testDate: { type: 'timestamp', isArray: false, isNullable: true }, updatedAt: { type: 'timestamp', isArray: false, isNullable: true }, + notes: { type: 'string', isArray: false, isNullable: true }, + roles: { type: 'number', isArray: true, isNullable: true }, + addresses: { type: 'number', isArray: true, isNullable: true }, + userGroup: { type: 'number', isArray: false, isNullable: true }, + manager: { type: 'number', isArray: false, isNullable: true }, + comments: { type: 'number', isArray: true, isNullable: true }, }; const fieldTypeAddresses: FieldWithType = { id: TypeField.number, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts index f6e67ffa..613fd01a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts @@ -1,4 +1,8 @@ -import { EntityProps, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { + EntityProps, + EntityRelation, + ObjectTyped, +} from '@klerick/json-api-nestjs-shared'; import { Repository } from 'typeorm'; import { ObjectLiteral } from '../../../types'; @@ -258,7 +262,7 @@ export const getPropsFromDb = ( repository: Repository ): PropsForField => { return repository.metadata.columns.reduce((acum, i) => { - const tmp = i.propertyName as unknown as EntityProps; + const tmp = i.propertyName as unknown as EntityProps & EntityRelation; acum[tmp] = { type: i.type as ColumnType, isArray: i.isArray, diff --git a/package-lock.json b/package-lock.json index 6087b0a3..bbf4ea72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0", "license": "MIT", "dependencies": { - "@anatine/zod-nestjs": "^2.0.5", "@anatine/zod-openapi": "^2.2.3", "@angular/animations": "19.0.3", "@angular/common": "19.0.3", @@ -131,25 +130,10 @@ "node": ">=6.0.0" } }, - "node_modules/@anatine/zod-nestjs": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@anatine/zod-nestjs/-/zod-nestjs-2.0.9.tgz", - "integrity": "sha512-XEK+7wMXAxc4tOkzOpH/vav1MVZrVYeOwKpXmn7aFiTUoB08G1FzAP7rDQ90ZrIFOGSoC0hpJA9izPQxBRAIDg==", - "dependencies": { - "ts-deepmerge": "^6.1.0" - }, - "peerDependencies": { - "@anatine/zod-openapi": "^2.0.1", - "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/swagger": "^6.0.0 || ^7.0.0", - "openapi3-ts": "^4.1.2", - "zod": "^3.20.0" - } - }, "node_modules/@anatine/zod-openapi": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@anatine/zod-openapi/-/zod-openapi-2.2.6.tgz", - "integrity": "sha512-Z5sr2Nq2xifEpPbPdUcvyl776LY652oR3VHMV++WFSmRrRL8RDP2XTkbuGn+vgfVNOD7UrndYwCWnxaiw7IZog==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@anatine/zod-openapi/-/zod-openapi-2.2.7.tgz", + "integrity": "sha512-kv/bGowgSGHNY2d/KIzx941ym0/elc7xoBiPri31qEUqbDPOSIppiMOZ88AedaTtLk5J1K96++h0CEsHkgFFyQ==", "dependencies": { "ts-deepmerge": "^6.0.3" }, @@ -20598,9 +20582,9 @@ } }, "node_modules/openapi3-ts/node_modules/yaml": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz", - "integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "peer": true, "bin": { "yaml": "bin.mjs" diff --git a/package.json b/package.json index 2dc49ddf..3f68ef59 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ }, "private": true, "dependencies": { - "@anatine/zod-nestjs": "^2.0.5", "@anatine/zod-openapi": "^2.2.3", "@angular/animations": "19.0.3", "@angular/common": "19.0.3", From 1ae25858069593ca786fa698c1217591304c386d Mon Sep 17 00:00:00 2001 From: Alex H Date: Sat, 25 Jan 2025 07:35:47 +0100 Subject: [PATCH 13/26] refactor(json-api-nestjs): add method for validate from microorm --- .env | 4 + .../json-api/json-api-sdk/atomic-sdk.spec.ts | 8 +- .../check-common-decorator.spec.ts | 2 +- .../json-api-sdk/check-othe-call.spec.ts | 2 +- .../json-api/json-api-sdk/get-method.spec.ts | 8 +- .../json-api-sdk/patch-methode.spec.ts | 7 +- .../json-api/json-api-sdk/post-method.spec.ts | 8 +- .../src/json-api/utils/data-utils.ts | 2 +- apps/json-api-server/src/app/app.module.ts | 10 +- .../app/resources/controllers/entity-orm.ts | 10 + .../extend-book-list.controller.ts | 8 +- .../extend-user/extend-user.controller.ts | 25 +- .../src/app/resources/resources.module.ts | 63 +- .../src/app/resources/service/example.pipe.ts | 2 +- libs/database/README.md | 7 - libs/database/project.json | 8 - libs/database/src/index.ts | 2 - libs/json-api/json-api-nestjs/src/index.ts | 2 +- .../json-api-nestjs/src/lib/constants/di.ts | 2 + .../src/lib/json-api.module.ts | 54 +- .../src/lib/mock-utils/index.ts | 59 -- .../mock-utils/microrom/entities/addresses.ts | 72 ++ .../mock-utils/microrom/entities/comments.ts | 53 ++ .../lib/mock-utils/microrom/entities/index.ts | 15 + .../lib/mock-utils/microrom/entities/notes.ts | 44 ++ .../lib/mock-utils/microrom/entities/roles.ts | 67 ++ .../microrom/entities/user-groups.ts | 30 + .../lib/mock-utils/microrom/entities/users.ts | 146 ++++ .../src/lib/mock-utils/microrom/index.ts | 37 + .../lib/mock-utils/microrom/utils/index.ts | 2 + .../microrom/utils/provider-entities.ts | 30 + .../mock-utils/microrom/utils/pull-data.ts | 134 ++++ .../{ => typeorm}/entities/addresses.ts | 0 .../{ => typeorm}/entities/comments.ts | 0 .../{ => typeorm}/entities/index.ts | 0 .../{ => typeorm}/entities/notes.ts | 0 .../mock-utils/{ => typeorm}/entities/pods.ts | 0 .../entities/requests-have-pod-locks.ts | 0 .../{ => typeorm}/entities/requests.ts | 0 .../{ => typeorm}/entities/roles.ts | 0 .../{ => typeorm}/entities/user-groups.ts | 0 .../{ => typeorm}/entities/users.ts | 0 .../mock-utils/{ => typeorm}/utils/index.ts | 0 .../{ => typeorm}/utils/provider-entities.ts | 2 +- .../{ => typeorm}/utils/pull-data.ts | 0 .../controllers/operation.controller.spec.ts | 10 +- .../utils/zod/zod-helper.spec.ts | 2 +- .../lib/modules/micro-orm/constants/index.ts | 2 + .../lib/modules/micro-orm/factory/index.ts | 182 +++++ .../src/lib/modules/micro-orm/index.ts | 2 +- .../micro-orm/micro-orm-json-api.module.ts | 57 ++ .../lib/modules/micro-orm/micro-orm.module.ts | 14 - .../micro-orm/orm-helper/index.spec.ts | 265 +++++++ .../lib/modules/micro-orm/orm-helper/index.ts | 218 ++++++ .../lib/modules/micro-orm/service/index.ts | 1 + .../micro-orm/service/microorm-service.ts | 66 ++ .../src/lib/modules/micro-orm/type.ts | 2 +- .../mixin/helper/bind-controller.spec.ts | 2 +- .../mixin/helper/create-controller.spec.ts | 2 +- .../mixin/interceptors/error.interceptors.ts | 2 +- .../query-check-select-field.spec.ts | 2 +- .../query-filed-in-include.pipe.spec.ts | 2 +- .../zod/zod-input-patch-schema/index.spec.ts | 67 +- .../zod/zod-input-post-schema/index.spec.ts | 80 +- .../zod/zod-input-query-schema/fields.spec.ts | 15 +- .../zod/zod-input-query-schema/filter.spec.ts | 52 +- .../zod/zod-input-query-schema/index.spec.ts | 52 +- .../mixin/zod/zod-query-schema/fields.spec.ts | 46 +- .../mixin/zod/zod-query-schema/filter.spec.ts | 114 +-- .../zod/zod-query-schema/include.spec.ts | 16 +- .../mixin/zod/zod-query-schema/index.spec.ts | 124 +-- .../mixin/zod/zod-query-schema/sort.spec.ts | 45 +- .../mixin/zod/zod-share/attributes.spec.ts | 52 +- .../mixin/zod/zod-share/relationships.spec.ts | 38 +- .../lib/modules/mixin/zod/zod-utils.spec.ts | 6 - .../src/lib/modules/type-orm/constants/di.ts | 2 - .../lib/modules/type-orm/constants/index.ts | 2 - .../src/lib/modules/type-orm/factory/index.ts | 32 +- .../src/lib/modules/type-orm/index.ts | 2 +- .../modules/type-orm/orm-helper/index.spec.ts | 24 +- .../orm-methods/delete-one/delete-one.spec.ts | 6 +- .../delete-relationship.spec.ts | 4 +- .../orm-methods/get-all/get-all.spec.ts | 4 +- .../orm-methods/get-one/get-one.spec.ts | 4 +- .../get-relationship/get-relationship.spec.ts | 4 +- .../orm-methods/patch-one/patch-one.spec.ts | 4 +- .../patch-relationship.spec.ts | 4 +- .../orm-methods/post-one/post-one.spec.ts | 4 +- .../post-relationship.spec.ts | 4 +- .../service/entity-props-map.service.spec.ts | 4 +- .../service/entity-props-map.service.ts | 2 +- .../service/transform-data.service.spec.ts | 4 +- .../type-orm/service/type-orm.service.ts | 6 +- .../service/typeorm-utils.service.spec.ts | 10 +- .../type-orm/service/typeorm-utils.service.ts | 2 +- ....module.ts => type-orm-json-api.module.ts} | 9 +- .../src/lib/types/module-common.types.ts | 43 +- .../src/lib/utils/___test___/test.helper.ts | 211 +++++ .../src/lib/utils/helper.spec.ts | 138 ++-- .../json-api-nestjs/src/lib/utils/helper.ts | 48 +- .../.eslintrc.json | 2 +- libs/microorm-database/README.md | 7 + libs/microorm-database/jest.config.ts | 10 + libs/microorm-database/project.json | 9 + libs/microorm-database/src/index.ts | 2 + libs/microorm-database/src/lib/config-cli.ts | 45 ++ libs/microorm-database/src/lib/config.ts | 10 + .../src/lib/entities/addresses.ts | 58 ++ .../src/lib/entities/book-list.ts | 51 ++ .../src/lib/entities/comments.ts | 53 ++ .../src/lib/entities/index.ts | 5 + .../src/lib/entities/roles.ts | 67 ++ .../src/lib/entities/users.ts | 105 +++ .../src/lib/micro-orm-database.module.ts | 10 + .../migrations/.snapshot-microorm-test.json | 726 ++++++++++++++++++ ...igration20250123104848_CreateUsersTable.ts | 14 + ...tion20250123105611_CreateAddressesTable.ts | 13 + ...igration20250123110115_CreateRolesTable.ts | 14 + ...ation20250123111042_CreateCommentsTable.ts | 16 + ...0250123123708_CreateUsersRolesRelations.ts | 16 + ...0250123124745_CreateUsersUsersRelations.ts | 18 + ...50123125941_CreateUsersAddressRelations.ts | 18 + ...0123130345_CreateUsersCommentsRelations.ts | 16 + ...ation20250123131039_CreateBookListTable.ts | 14 + ...0123131438_CreateUsersBookListRelations.ts | 16 + libs/microorm-database/tsconfig.json | 22 + libs/microorm-database/tsconfig.lib.json | 16 + libs/microorm-database/tsconfig.spec.json | 15 + libs/typeorm-database/.eslintrc.json | 18 + libs/typeorm-database/README.md | 7 + .../jest.config.ts | 4 +- libs/typeorm-database/project.json | 9 + libs/typeorm-database/src/index.ts | 2 + .../src/lib/config-cli.ts | 0 .../src/lib/config.ts | 0 .../src/lib/entities/addresses.ts | 0 .../src/lib/entities/book-list.ts | 0 .../src/lib/entities/comments.ts | 0 .../src/lib/entities/index.ts | 0 .../src/lib/entities/roles.ts | 0 .../src/lib/entities/users-have-roles.ts | 0 .../src/lib/entities/users.ts | 0 .../1607701631900-CreateAddressesTable.ts | 0 .../1607701632000-CreateUsersTable.ts | 0 .../1607701632200-CreateRolesTable.ts | 0 ...1607701632300-CreateUsersHaveRolesTable.ts | 0 .../1607701632600-CreateCommentsTable.ts | 0 .../1665469071344-CreateBookTable.ts | 0 .../1665719467563-CreateUsersHasBookTable.ts | 0 .../lib/seeders/factory/addresses.factory.ts | 0 .../lib/seeders/factory/comments.factory.ts | 0 .../src/lib/seeders/factory/index.ts | 0 .../src/lib/seeders/factory/roles.factory.ts | 0 .../src/lib/seeders/factory/user.factory.ts | 0 .../src/lib/seeders/root.seeder.ts | 0 .../src/lib/type-orm-database.module.ts} | 2 +- .../tsconfig.json | 0 .../tsconfig.lib.json | 0 .../tsconfig.spec.json | 0 package-lock.json | 364 ++++++++- package.json | 14 +- tsconfig.base.json | 7 +- 162 files changed, 3879 insertions(+), 956 deletions(-) create mode 100644 apps/json-api-server/src/app/resources/controllers/entity-orm.ts delete mode 100644 libs/database/README.md delete mode 100644 libs/database/project.json delete mode 100644 libs/database/src/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/addresses.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/roles.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/user-groups.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/users.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/provider-entities.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/addresses.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/comments.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/index.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/notes.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/pods.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/requests-have-pod-locks.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/requests.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/roles.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/user-groups.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/entities/users.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/utils/index.ts (100%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/utils/provider-entities.ts (96%) rename libs/json-api/json-api-nestjs/src/lib/mock-utils/{ => typeorm}/utils/pull-data.ts (100%) create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/constants/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts rename libs/json-api/json-api-nestjs/src/lib/modules/type-orm/{type-orm.module.ts => type-orm-json-api.module.ts} (88%) create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/___test___/test.helper.ts rename libs/{database => microorm-database}/.eslintrc.json (85%) create mode 100644 libs/microorm-database/README.md create mode 100644 libs/microorm-database/jest.config.ts create mode 100644 libs/microorm-database/project.json create mode 100644 libs/microorm-database/src/index.ts create mode 100644 libs/microorm-database/src/lib/config-cli.ts create mode 100644 libs/microorm-database/src/lib/config.ts create mode 100644 libs/microorm-database/src/lib/entities/addresses.ts create mode 100644 libs/microorm-database/src/lib/entities/book-list.ts create mode 100644 libs/microorm-database/src/lib/entities/comments.ts create mode 100644 libs/microorm-database/src/lib/entities/index.ts create mode 100644 libs/microorm-database/src/lib/entities/roles.ts create mode 100644 libs/microorm-database/src/lib/entities/users.ts create mode 100644 libs/microorm-database/src/lib/micro-orm-database.module.ts create mode 100644 libs/microorm-database/src/lib/migrations/.snapshot-microorm-test.json create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123104848_CreateUsersTable.ts create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123105611_CreateAddressesTable.ts create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123110115_CreateRolesTable.ts create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123111042_CreateCommentsTable.ts create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123124745_CreateUsersUsersRelations.ts create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123125941_CreateUsersAddressRelations.ts create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123130345_CreateUsersCommentsRelations.ts create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123131039_CreateBookListTable.ts create mode 100644 libs/microorm-database/src/lib/migrations/Migration20250123131438_CreateUsersBookListRelations.ts create mode 100644 libs/microorm-database/tsconfig.json create mode 100644 libs/microorm-database/tsconfig.lib.json create mode 100644 libs/microorm-database/tsconfig.spec.json create mode 100644 libs/typeorm-database/.eslintrc.json create mode 100644 libs/typeorm-database/README.md rename libs/{database => typeorm-database}/jest.config.ts (72%) create mode 100644 libs/typeorm-database/project.json create mode 100644 libs/typeorm-database/src/index.ts rename libs/{database => typeorm-database}/src/lib/config-cli.ts (100%) rename libs/{database => typeorm-database}/src/lib/config.ts (100%) rename libs/{database => typeorm-database}/src/lib/entities/addresses.ts (100%) rename libs/{database => typeorm-database}/src/lib/entities/book-list.ts (100%) rename libs/{database => typeorm-database}/src/lib/entities/comments.ts (100%) rename libs/{database => typeorm-database}/src/lib/entities/index.ts (100%) rename libs/{database => typeorm-database}/src/lib/entities/roles.ts (100%) rename libs/{database => typeorm-database}/src/lib/entities/users-have-roles.ts (100%) rename libs/{database => typeorm-database}/src/lib/entities/users.ts (100%) rename libs/{database => typeorm-database}/src/lib/migrations/1607701631900-CreateAddressesTable.ts (100%) rename libs/{database => typeorm-database}/src/lib/migrations/1607701632000-CreateUsersTable.ts (100%) rename libs/{database => typeorm-database}/src/lib/migrations/1607701632200-CreateRolesTable.ts (100%) rename libs/{database => typeorm-database}/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts (100%) rename libs/{database => typeorm-database}/src/lib/migrations/1607701632600-CreateCommentsTable.ts (100%) rename libs/{database => typeorm-database}/src/lib/migrations/1665469071344-CreateBookTable.ts (100%) rename libs/{database => typeorm-database}/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts (100%) rename libs/{database => typeorm-database}/src/lib/seeders/factory/addresses.factory.ts (100%) rename libs/{database => typeorm-database}/src/lib/seeders/factory/comments.factory.ts (100%) rename libs/{database => typeorm-database}/src/lib/seeders/factory/index.ts (100%) rename libs/{database => typeorm-database}/src/lib/seeders/factory/roles.factory.ts (100%) rename libs/{database => typeorm-database}/src/lib/seeders/factory/user.factory.ts (100%) rename libs/{database => typeorm-database}/src/lib/seeders/root.seeder.ts (100%) rename libs/{database/src/lib/database.module.ts => typeorm-database/src/lib/type-orm-database.module.ts} (84%) rename libs/{database => typeorm-database}/tsconfig.json (100%) rename libs/{database => typeorm-database}/tsconfig.lib.json (100%) rename libs/{database => typeorm-database}/tsconfig.spec.json (100%) diff --git a/.env b/.env index 6aa8c6ae..4a927090 100644 --- a/.env +++ b/.env @@ -13,3 +13,7 @@ DB_TYPE=postgres #DB_NAME="example_new" #DB_PORT=3306 #DB_TYPE=mysql + + +ORM_TYPE=microorm +#ORM_TYPE=typeorm diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts index 9551718b..b89a48c5 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/atomic-sdk.spec.ts @@ -1,6 +1,12 @@ import { INestApplication } from '@nestjs/common'; import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; -import { Addresses, CommentKind, Comments, Roles, Users } from 'database'; +import { + Addresses, + CommentKind, + Comments, + Roles, + Users, +} from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; import { getUser } from '../utils/data-utils'; import { run, creatSdk } from '../utils/run-application'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts index 91eb763e..969858db 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-common-decorator.spec.ts @@ -1,7 +1,7 @@ import { INestApplication } from '@nestjs/common'; import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; import { AxiosError } from 'axios'; -import { Users } from 'database'; +import { Users } from '@nestjs-json-api/typeorm-database'; import { run, creatSdk } from '../utils/run-application'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts index abf3c40d..d6d2957a 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/check-othe-call.spec.ts @@ -1,6 +1,6 @@ import { INestApplication } from '@nestjs/common'; import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; -import { BookList, Users } from 'database'; +import { BookList, Users } from '@nestjs-json-api/typeorm-database'; import { AxiosError } from 'axios'; import { faker } from '@faker-js/faker'; import { lastValueFrom } from 'rxjs'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts index d64f62cb..cd12fe45 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts @@ -1,5 +1,11 @@ import { INestApplication } from '@nestjs/common'; -import { Addresses, CommentKind, Comments, Roles, Users } from 'database'; +import { + Addresses, + CommentKind, + Comments, + Roles, + Users, +} from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; import { FilterOperand, JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts index 720828d9..09289a0f 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/patch-methode.spec.ts @@ -1,5 +1,10 @@ import { INestApplication } from '@nestjs/common'; -import { Addresses, CommentKind, Comments, Users } from 'database'; +import { + Addresses, + CommentKind, + Comments, + Users, +} from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; import { JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts index e20b86d9..226947cf 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/post-method.spec.ts @@ -1,4 +1,10 @@ -import { Addresses, BookList, CommentKind, Comments, Users } from 'database'; +import { + Addresses, + BookList, + CommentKind, + Comments, + Users, +} from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; import { JsonSdkPromise } from '@klerick/json-api-nestjs-sdk'; diff --git a/apps/json-api-server-e2e/src/json-api/utils/data-utils.ts b/apps/json-api-server-e2e/src/json-api/utils/data-utils.ts index 89bc0937..7e4cef3b 100644 --- a/apps/json-api-server-e2e/src/json-api/utils/data-utils.ts +++ b/apps/json-api-server-e2e/src/json-api/utils/data-utils.ts @@ -1,4 +1,4 @@ -import { Users } from 'database'; +import { Users } from '@nestjs-json-api/typeorm-database'; import { faker } from '@faker-js/faker'; export const getUser = () => { diff --git a/apps/json-api-server/src/app/app.module.ts b/apps/json-api-server/src/app/app.module.ts index 45403244..9c39043a 100644 --- a/apps/json-api-server/src/app/app.module.ts +++ b/apps/json-api-server/src/app/app.module.ts @@ -1,14 +1,20 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from 'nestjs-pino'; -import { DatabaseModule } from 'database'; +import { TypeOrmDatabaseModule } from '@nestjs-json-api/typeorm-database'; +import { MicroOrmDatabaseModule } from '@nestjs-json-api/microorm-database'; import { ResourcesModule } from './resources/resources.module'; import { RpcModule } from './rpc/rpc.module'; import * as process from 'process'; +const ormModule = + process.env['ORM_TYPE'] === 'typeorm' + ? TypeOrmDatabaseModule + : MicroOrmDatabaseModule; + @Module({ imports: [ - DatabaseModule, + ormModule, ResourcesModule, RpcModule, LoggerModule.forRoot({ diff --git a/apps/json-api-server/src/app/resources/controllers/entity-orm.ts b/apps/json-api-server/src/app/resources/controllers/entity-orm.ts new file mode 100644 index 00000000..97042f97 --- /dev/null +++ b/apps/json-api-server/src/app/resources/controllers/entity-orm.ts @@ -0,0 +1,10 @@ +import { Users as tUsers } from '@nestjs-json-api/typeorm-database'; +import { Users as mkUsers } from '@nestjs-json-api/microorm-database'; + +import { BookList as tBookList } from '@nestjs-json-api/typeorm-database'; +import { BookList as mkBookList } from '@nestjs-json-api/microorm-database'; + +const Users = process.env['ORM_TYPE'] === 'typeorm' ? tUsers : mkUsers; +const BookList = process.env['ORM_TYPE'] === 'typeorm' ? tBookList : tBookList; + +export { Users, BookList }; diff --git a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts b/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts index 612f74e3..91b59a04 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts +++ b/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts @@ -1,10 +1,12 @@ import { ParseUUIDPipe } from '@nestjs/common'; -import { BookList } from 'database'; +import { BookList } from '../entity-orm'; import { JsonApi, JsonBaseController } from '@klerick/json-api-nestjs'; -@JsonApi(BookList, { +@JsonApi(BookList as typeof BookList, { pipeForId: ParseUUIDPipe, overrideRoute: 'override-book-list', allowMethod: ['getOne', 'postOne', 'deleteOne'], }) -export class ExtendBookListController extends JsonBaseController {} +export class ExtendBookListController extends JsonBaseController< + typeof BookList +> {} diff --git a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts b/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts index 00c4ea1b..6682ba36 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts +++ b/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts @@ -7,7 +7,7 @@ import { UseFilters, UseGuards, } from '@nestjs/common'; -import { Users } from 'database'; + import { JsonApi, JsonBaseController, @@ -29,32 +29,35 @@ import { HttpExceptionMethodFilter, } from '../../service/http-exception.filter'; import { GuardService, EntityName } from '../../service/guard.service'; +import { Users } from '../entity-orm'; import { AtomicInterceptor } from '../../service/atomic.interceptor'; @UseGuards(GuardService) @UseFilters(new HttpExceptionFilter()) @UseInterceptors(ControllerInterceptor) -@JsonApi(Users) -export class ExtendUserController extends JsonBaseController { - @InjectService() public service: JsonApiService; +@JsonApi(Users as any) +export class ExtendUserController extends JsonBaseController { + @InjectService() public service: JsonApiService; @Inject(ExampleService) protected exampleService: ExampleService; getOne( id: string | number, - query: QueryType - ): Promise> { + query: QueryType + ): Promise> { return super.getOne(id, query); } - patchRelationship>( + patchRelationship>( id: string | number, relName: Rel, input: PatchRelationshipData - ): Promise> { + ): Promise> { return super.patchRelationship(id, relName, input); } // @UseInterceptors(AtomicInterceptor) - postOne(inputData: PostData): Promise> { + postOne( + inputData: PostData + ): Promise> { return super.postOne(inputData); } @@ -62,8 +65,8 @@ export class ExtendUserController extends JsonBaseController { @UseFilters(HttpExceptionMethodFilter) @UseInterceptors(MethodInterceptor) getAll( - @Query(ExamplePipe) query: QueryType - ): Promise> { + @Query(ExamplePipe) query: QueryType + ): Promise> { return super.getAll(query); } diff --git a/apps/json-api-server/src/app/resources/resources.module.ts b/apps/json-api-server/src/app/resources/resources.module.ts index 6a6b5e2e..38cccd0b 100644 --- a/apps/json-api-server/src/app/resources/resources.module.ts +++ b/apps/json-api-server/src/app/resources/resources.module.ts @@ -1,23 +1,56 @@ import { Module } from '@nestjs/common'; -import { JsonApiModule, TypeOrmModule } from '@klerick/json-api-nestjs'; -import { Users, Addresses, Comments, Roles, BookList } from 'database'; +import { + JsonApiModule, + MicroOrmJsonApiModule, + TypeOrmJsonApiModule, +} from '@klerick/json-api-nestjs'; +import { + Users, + Addresses, + Comments, + Roles, + BookList, +} from '@nestjs-json-api/typeorm-database'; + +import { + Users as mkUsers, + Addresses as mkAddresses, + Comments as mkComments, + Roles as mkRoles, + BookList as mkBookList, +} from '@nestjs-json-api/microorm-database'; + import { ExtendBookListController } from './controllers/extend-book-list/extend-book-list.controller'; import { ExtendUserController } from './controllers/extend-user/extend-user.controller'; import { ExampleService } from './service/example.service'; +const typeOrm = () => + JsonApiModule.forRoot(TypeOrmJsonApiModule, { + entities: [Users, Addresses, Comments, Roles, BookList], + controllers: [ExtendBookListController, ExtendUserController], + providers: [ExampleService], + options: { + debug: true, + requiredSelectField: false, + operationUrl: 'operation', + }, + }); + +const microOrm = () => + JsonApiModule.forRoot(MicroOrmJsonApiModule, { + entities: [mkUsers, mkAddresses, mkComments, mkRoles, mkBookList], + controllers: [ExtendBookListController, ExtendUserController], + providers: [ExampleService], + options: { + debug: true, + requiredSelectField: false, + operationUrl: 'operation', + }, + }); + +const ormModule = process.env['ORM_TYPE'] === 'typeorm' ? typeOrm : microOrm; + @Module({ - imports: [ - JsonApiModule.forRoot({ - entities: [Users, Addresses, Comments, Roles, BookList], - controllers: [ExtendBookListController, ExtendUserController], - providers: [ExampleService], - type: TypeOrmModule, - options: { - debug: true, - requiredSelectField: false, - operationUrl: 'operation', - }, - }), - ], + imports: [ormModule()], }) export class ResourcesModule {} diff --git a/apps/json-api-server/src/app/resources/service/example.pipe.ts b/apps/json-api-server/src/app/resources/service/example.pipe.ts index 7cdf2399..9adda16d 100644 --- a/apps/json-api-server/src/app/resources/service/example.pipe.ts +++ b/apps/json-api-server/src/app/resources/service/example.pipe.ts @@ -5,7 +5,7 @@ import { } from '@nestjs/common'; import { Query } from '@klerick/json-api-nestjs'; -import { Users } from 'database'; +import { Users } from '@nestjs-json-api/typeorm-database'; export class ExamplePipe implements PipeTransform, Query> { transform(value: Query, metadata: ArgumentMetadata): Query { diff --git a/libs/database/README.md b/libs/database/README.md deleted file mode 100644 index 8453478f..00000000 --- a/libs/database/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# database - -This library was generated with [Nx](https://nx.dev). - -## Running unit tests - -Run `nx test database` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/database/project.json b/libs/database/project.json deleted file mode 100644 index 43dc1750..00000000 --- a/libs/database/project.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "database", - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/database/src", - "projectType": "library", - "targets": {}, - "tags": [] -} diff --git a/libs/database/src/index.ts b/libs/database/src/index.ts deleted file mode 100644 index 0207b998..00000000 --- a/libs/database/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './lib/database.module'; -export * from './lib/entities'; diff --git a/libs/json-api/json-api-nestjs/src/index.ts b/libs/json-api/json-api-nestjs/src/index.ts index e8d912cd..c9d708b3 100644 --- a/libs/json-api/json-api-nestjs/src/index.ts +++ b/libs/json-api/json-api-nestjs/src/index.ts @@ -1,5 +1,5 @@ export { JsonApiModule } from './lib/json-api.module'; -export { TypeOrmModule, MicroOrmModule } from './lib/modules'; +export { TypeOrmJsonApiModule, MicroOrmJsonApiModule } from './lib/modules'; export { JsonApi, InjectService } from './lib/modules/mixin/decorators'; export { OrmService as JsonApiService } from './lib/modules/mixin/types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/di.ts b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts index ec21865e..ed2d39a1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/constants/di.ts +++ b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts @@ -26,3 +26,5 @@ export const ZOD_POST_RELATIONSHIP_SCHEMA = Symbol( export const ZOD_PATCH_RELATIONSHIP_SCHEMA = Symbol( 'ZOD_PATCH_RELATIONSHIP_SCHEMA' ); +export const CURRENT_DATA_SOURCE_TOKEN = Symbol('CURRENT_DATA_SOURCE_TOKEN'); +export const CURRENT_ENTITY_REPOSITORY = Symbol('CURRENT_ENTITY_REPOSITORY'); diff --git a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts index d14b20fd..0e9fad09 100644 --- a/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts +++ b/libs/json-api/json-api-nestjs/src/lib/json-api.module.ts @@ -1,13 +1,61 @@ import { DynamicModule, Module } from '@nestjs/common'; import { DiscoveryModule } from '@nestjs/core'; -import { AnyEntity, EntityName, ModuleOptions } from './types'; +import { + AnyEntity, + EntityName, + TypeOrmDefaultOptions, + TypeOrmOptions, + MicroOrmOptions, + ResultModuleOptions, +} from './types'; import { createMixinModule, prepareConfig, createAtomicModule } from './utils'; +import type { TypeOrmJsonApiModule, MicroOrmJsonApiModule } from './modules'; @Module({}) export class JsonApiModule { - public static forRoot(options: ModuleOptions): DynamicModule { - const resultOption = prepareConfig(options); + public static forRoot( + module: typeof TypeOrmJsonApiModule, + options: TypeOrmOptions + ): DynamicModule; + public static forRoot( + module: typeof MicroOrmJsonApiModule, + options: MicroOrmOptions + ): DynamicModule; + /** + * @deprecated This type of method is deprecated and may be removed in future versions. + * Consider using newer alternatives or updated patterns for module registration. + */ + public static forRoot(options: TypeOrmDefaultOptions): DynamicModule; + public static forRoot( + first: + | typeof TypeOrmJsonApiModule + | typeof MicroOrmJsonApiModule + | TypeOrmDefaultOptions, + second?: TypeOrmOptions | MicroOrmOptions + ): DynamicModule { + let resultOption: ResultModuleOptions = {} as any; + + if (second) { + const module = first as + | typeof TypeOrmJsonApiModule + | typeof MicroOrmJsonApiModule; + resultOption = { + ...prepareConfig(second, module.module), + type: module, + } as ResultModuleOptions; + } else { + const { + TypeOrmJsonApiModule, + } = require('./modules/type-orm/type-orm-json-api.module'); + resultOption = { + ...prepareConfig( + first as TypeOrmDefaultOptions, + TypeOrmJsonApiModule.module + ), + type: TypeOrmJsonApiModule as typeof TypeOrmJsonApiModule, + } as any; + } resultOption.imports.unshift(DiscoveryModule); diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts index bd6b43f6..55385b02 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts @@ -1,39 +1,8 @@ -import { TypeOrmModule } from '@nestjs/typeorm'; -import { DynamicModule } from '@nestjs/common'; import { DataType, IMemoryDb, newDb } from 'pg-mem'; import { readFileSync } from 'fs'; import { join } from 'path'; - -import { - Users, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - UserGroups, - Notes, -} from './entities'; -import { DataSource } from 'typeorm'; - import { v4 } from 'uuid'; -export * from './entities'; -export * from './utils'; - -export const entities = [ - Users, - UserGroups, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - Notes, -]; - export function createAndPullSchemaBase(): IMemoryDb { const dump = readFileSync(join(__dirname, 'db-for-test'), { encoding: 'utf8', @@ -64,31 +33,3 @@ export function createAndPullSchemaBase(): IMemoryDb { db.public.none(dump); return db; } -export function mockDBTestModule(db: IMemoryDb): DynamicModule { - return TypeOrmModule.forRootAsync({ - useFactory() { - return { - type: 'postgres', - // logging: true, - entities: [ - Users, - UserGroups, - Roles, - RequestsHavePodLocks, - Requests, - Pods, - Comments, - Addresses, - Notes, - ], - }; - }, - async dataSourceFactory(options) { - const dataSource: DataSource = await db.adapters.createTypeormDataSource( - options - ); - - return dataSource; - }, - }); -} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/addresses.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/addresses.ts new file mode 100644 index 00000000..7952b916 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/addresses.ts @@ -0,0 +1,72 @@ +import { + Entity, + PrimaryKey, + Property, + OneToOne, + ArrayType, +} from '@mikro-orm/core'; + +import { Users, IUsers } from '.'; + +export type IAddresses = Addresses; + +@Entity({ + tableName: 'addresses', +}) +export class Addresses { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + columnType: 'varchar', + length: 70, + nullable: true, + }) + public city!: string; + + @Property({ + columnType: 'varchar', + length: 70, + nullable: true, + }) + public state!: string; + + @Property({ + columnType: 'varchar', + length: 68, + nullable: true, + }) + public country!: string; + + @Property({ + name: 'array_field', + type: ArrayType, + columnType: 'varchar[]', + nullable: true, + }) + public arrayField!: string[]; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @OneToOne(() => Users, (item) => item.addresses) + public user!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts new file mode 100644 index 00000000..2b354d6d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts @@ -0,0 +1,53 @@ +import { Entity, PrimaryKey, Property, Enum, ManyToOne } from '@mikro-orm/core'; + +export enum CommentKind { + Comment = 'COMMENT', + Message = 'MESSAGE', + Note = 'NOTE', +} + +import { Users, IUsers } from '.'; + +export type IComments = Comments; + +@Entity({ + tableName: 'comments', +}) +export class Comments { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + columnType: 'text', + }) + public text!: string; + + @Enum({ items: () => CommentKind, nativeEnumName: 'comment_kind_enum' }) + public kind!: CommentKind; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToOne(() => Users, { + fieldName: 'created_by', + }) + createdBy!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/index.ts new file mode 100644 index 00000000..451e1a81 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/index.ts @@ -0,0 +1,15 @@ +export * from './users'; +export * from './roles'; +export * from './comments'; +export * from './addresses'; +export * from './user-groups'; +export * from './notes'; + +import { Users } from './users'; +import { Roles } from './roles'; +import { Comments } from './comments'; +import { Addresses } from './addresses'; +import { UserGroups } from './user-groups'; +import { Notes } from './notes'; + +export const Entities = [Users, Roles, Comments, Addresses, UserGroups, Notes]; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts new file mode 100644 index 00000000..f79aa30e --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts @@ -0,0 +1,44 @@ +import { PrimaryKey, Property, Entity, ManyToOne } from '@mikro-orm/core'; + +import { Users, IUsers } from './index'; + +@Entity({ + tableName: 'notes', +}) +export class Notes { + @PrimaryKey({ + type: 'uuid', + defaultRaw: 'uuid_generate_v4()', + }) + public id!: string; + + @Property({ + type: 'text', + nullable: false, + }) + public text!: string; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToOne(() => Users, { + fieldName: 'created_by', + }) + public createdBy!: IUsers; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/roles.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/roles.ts new file mode 100644 index 00000000..3dcd026d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/roles.ts @@ -0,0 +1,67 @@ +import { + Entity, + PrimaryKey, + Property, + ManyToMany, + Collection, +} from '@mikro-orm/core'; + +import { Users, IUsers } from '.'; + +export type IRoles = Roles; + +@Entity({ + tableName: 'roles', +}) +export class Roles { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'varchar', + length: 128, + nullable: true, + default: 'NULL', + }) + public name!: string; + + @Property({ + type: 'varchar', + length: 128, + nullable: false, + unique: true, + }) + public key!: string; + + @Property({ + name: 'is_default', + type: 'boolean', + default: false, + }) + public isDefault!: boolean; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + type: 'timestamp', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToMany(() => Users, (item) => item.roles) + public users = new Collection(this); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/user-groups.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/user-groups.ts new file mode 100644 index 00000000..b62ac057 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/user-groups.ts @@ -0,0 +1,30 @@ +import { + PrimaryKey, + OneToMany, + Entity, + Property, + Collection, +} from '@mikro-orm/core'; + +import { Users } from './index'; + +@Entity({ + tableName: 'user_groups', +}) +export class UserGroups { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'string', + length: 50, + unique: true, + columnType: 'varchar', + }) + public label!: string; + + @OneToMany(() => Users, (item) => item.userGroup) + public users = new Collection(this); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/users.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/users.ts new file mode 100644 index 00000000..08d1f31c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/users.ts @@ -0,0 +1,146 @@ +import { + Entity, + PrimaryKey, + Property, + OneToOne, + Collection, + ManyToMany, + OneToMany, + ManyToOne, + ArrayType, + Type, +} from '@mikro-orm/core'; + +import { + Addresses, + Roles, + Comments, + Notes, + UserGroups, + IAddresses, +} from './index'; + +export type IUsers = Users; + +export class MyDateType extends Type {} + +@Entity({ + tableName: 'users', +}) +export class Users { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'string', + length: 100, + unique: true, + }) + public login!: string; + + @Property({ + name: 'first_name', + type: 'varchar', + length: 100, + nullable: true, + }) + public firstName!: string; + + @Property({ + name: 'test_real', + type: new ArrayType((i) => parseFloat(i)), + columnType: 'real[]', + defaultRaw: `ARRAY[]::real[]`, + default: [], + }) + public testReal: number[] = []; + + @Property({ + name: 'test_array_null', + type: new ArrayType((i) => parseFloat(i)), + columnType: 'real[]', + nullable: true, + }) + public testArrayNull!: number[] | null; + + @Property({ + name: 'last_name', + type: 'string', + columnType: 'varchar', + length: 100, + nullable: true, + }) + public lastName!: string; + + @Property({ + name: 'is_active', + type: 'boolean', + nullable: true, + default: false, + }) + public isActive!: boolean; + + @Property({ + name: 'test_date', + type: Date, + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + public testDate!: Date; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @OneToOne(() => Addresses, { + owner: true, + fieldName: 'addresses_id', + nullable: true, + }) + public addresses!: IAddresses; + + @OneToOne(() => Users, { + owner: true, + nullable: true, + fieldName: 'manager_id', + }) + public manager!: IUsers; + + @ManyToMany(() => Roles, (role) => role.users, { + owner: true, + pivotTable: 'users_have_roles', + inverseJoinColumn: 'role_id', + joinColumn: 'user_id', + }) + public roles = new Collection(this); + + @OneToMany(() => Comments, (comment) => comment.createdBy) + public comments = new Collection(this); + + @OneToMany(() => Notes, (item) => item.createdBy) + public notes = new Collection(this); + + @ManyToOne(() => UserGroups, { + fieldName: 'user_groups_id', + nullable: true, + }) + public userGroup!: UserGroups; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts new file mode 100644 index 00000000..f7d5aa29 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts @@ -0,0 +1,37 @@ +import { DynamicModule } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { MikroORM } from '@mikro-orm/core'; +import { PostgreSqlDriver } from '@mikro-orm/postgresql'; + +import { IMemoryDb } from 'pg-mem'; + +import { + Addresses, + Comments, + Notes, + Roles, + UserGroups, + Users, +} from './entities'; + +export * from './entities'; +export * from './utils'; + +export const entities = [Users, UserGroups, Roles, Comments, Addresses, Notes]; + +export function mockDBTestModule(db: IMemoryDb): DynamicModule { + const mikroORM = { + provide: MikroORM, + useFactory: () => + db.adapters.createMikroOrm({ + entities: [Users, UserGroups, Roles, Comments, Addresses, Notes], + driver: PostgreSqlDriver, + allowGlobalContext: true, + }), + }; + return { + module: MikroOrmModule, + providers: [mikroORM], + exports: [mikroORM], + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts new file mode 100644 index 00000000..902c50a6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts @@ -0,0 +1,2 @@ +export * from './provider-entities'; +export * from './pull-data'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/provider-entities.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/provider-entities.ts new file mode 100644 index 00000000..44087e7a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/provider-entities.ts @@ -0,0 +1,30 @@ +import { TestingModule } from '@nestjs/testing'; +import { EntityManager } from '@mikro-orm/core'; +import { + Users, + Addresses, + Comments, + Roles, + UserGroups, + Notes, +} from '../entities'; + +export function getRepository(module: TestingModule, emToken: symbol) { + const em = module.get(emToken); + + const userRepository = em.getRepository(Users); + const addressesRepository = em.getRepository(Addresses); + const notesRepository = em.getRepository(Notes); + const commentsRepository = em.getRepository(Comments); + const rolesRepository = em.getRepository(Roles); + const userGroupRepository = em.getRepository(UserGroups); + + return { + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts new file mode 100644 index 00000000..c0acd551 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts @@ -0,0 +1,134 @@ +import { EntityManager } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; +import { + Addresses, + CommentKind, + Comments, + Notes, + Roles, + UserGroups, + Users, +} from '../entities'; + +export async function pullAddress() { + const address = new Addresses(); + address.city = faker.location.city(); + address.country = faker.location.country(); + address.arrayField = [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ]; + address.state = faker.location.state(); + return address; +} + +export async function pullComment() { + const comment = new Comments(); + comment.text = faker.lorem.paragraph(faker.number.int(5)); + comment.kind = CommentKind.Comment; + return comment; +} + +export async function pullNote() { + const note = new Notes(); + note.text = faker.lorem.paragraph(faker.number.int(5)); + return note; +} + +export async function pullRole() { + const role = new Roles(); + role.key = faker.string.alphanumeric(5); + role.name = faker.string.alphanumeric(5); + return role; +} + +export async function pullUser() { + const user = new Users(); + user.firstName = faker.person.firstName(); + user.lastName = faker.person.lastName(); + user.isActive = faker.datatype.boolean(); + user.login = faker.internet.userName({ + lastName: user.lastName, + firstName: user.firstName, + }); + user.testReal = [faker.number.float({ fractionDigits: 4 })]; + user.testArrayNull = null; + + user.testDate = faker.date.anytime(); + + return user; +} + +export async function pullUserGroup() { + const userGroup = new UserGroups(); + userGroup.label = faker.string.alphanumeric(5); + return userGroup; +} + +export async function pullAllData(em: EntityManager) { + const user = await pullUser(); + + const address1 = await pullAddress(); + const address2 = await pullAddress(); + + const note1 = await pullNote(); + const note2 = await pullNote(); + const note3 = await pullNote(); + + const comment1 = await pullComment(); + const comment2 = await pullComment(); + const comment3 = await pullComment(); + const comment4 = await pullComment(); + + const userGroup1 = await pullUserGroup(); + const userGroup2 = await pullUserGroup(); + const userGroup3 = await pullUserGroup(); + + const role1 = await pullRole(); + const role2 = await pullRole(); + const role3 = await pullRole(); + + const roleX1 = await pullRole(); + const roleX2 = await pullRole(); + const roleX3 = await pullRole(); + + const managerUser = await pullUser(); + + user.addresses = address1; + address1.user = user; + user.notes.add(note1, note2, note3); + user.comments.add(comment1, comment2, comment3, comment4); + user.userGroup = userGroup1; + user.roles.add(roleX1, roleX2, roleX3); + user.manager = managerUser; + + managerUser.addresses = address2; + managerUser.userGroup = userGroup3; + + await em.persistAndFlush([ + user, + address1, + address2, + note1, + note2, + note3, + comment1, + comment2, + comment3, + comment4, + userGroup1, + userGroup2, + userGroup3, + role1, + role2, + role3, + roleX1, + roleX2, + roleX3, + managerUser, + ]); + + await em.flush(); + + return user; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/addresses.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/addresses.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/addresses.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/comments.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/comments.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/comments.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/index.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/notes.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/notes.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/notes.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/pods.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/pods.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/pods.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/requests-have-pod-locks.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests-have-pod-locks.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/requests-have-pod-locks.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/requests.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/requests.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/requests.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/roles.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/roles.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/roles.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/user-groups.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/user-groups.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/user-groups.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/users.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/entities/users.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/users.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/index.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/index.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/provider-entities.ts similarity index 96% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/provider-entities.ts index 4b47577b..f0bca9e1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/provider-entities.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/provider-entities.ts @@ -12,7 +12,7 @@ import { UserGroups, } from '../entities'; import { Users } from '../entities'; -import { DEFAULT_CONNECTION_NAME } from '../../constants'; +import { DEFAULT_CONNECTION_NAME } from '../../../constants'; import { TestingModule } from '@nestjs/testing'; export function providerEntities( diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/pull-data.ts similarity index 100% rename from libs/json-api/json-api-nestjs/src/lib/mock-utils/utils/pull-data.ts rename to libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/utils/pull-data.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts index d080815b..1206658c 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/controllers/operation.controller.spec.ts @@ -10,11 +10,10 @@ import { ExecuteService, ExplorerService } from '../service'; import { InputArray, Operation } from '../utils'; import { JsonBaseController } from '../../mixin/controller/json-base.controller'; import { - createAndPullSchemaBase, mockDBTestModule, providerEntities, Users, -} from '../../../mock-utils'; +} from '../../../mock-utils/typeorm'; import { ASYNC_ITERATOR_FACTORY, @@ -28,9 +27,12 @@ import { import { OperationMethode } from '../types'; import { AsyncLocalStorage } from 'async_hooks'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../type-orm/constants'; import { ObjectLiteral } from '../../../types'; -import { RUN_IN_TRANSACTION_FUNCTION } from '../../../constants'; +import { + CURRENT_DATA_SOURCE_TOKEN, + RUN_IN_TRANSACTION_FUNCTION, +} from '../../../constants'; +import { createAndPullSchemaBase } from '../../../mock-utils'; describe('OperationController', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts index 9419ce57..62b545d5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts @@ -13,7 +13,7 @@ import { zodUpdate, ZodUpdate, } from './zod-helper'; -import { Users } from '../../../../mock-utils'; +import { Users } from '../../../../mock-utils/typeorm'; import { FIELD_FOR_ENTITY } from '../../../../constants'; import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; import { MapController } from '../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/constants/index.ts new file mode 100644 index 00000000..116b14aa --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/constants/index.ts @@ -0,0 +1,2 @@ +export const ENTITY_METADATA_TOKEN = Symbol('ENTITY_METADATA_TOKEN'); +export const DEFAULT_ARRAY_TYPE = ['ArrayType', 'EnumArrayType']; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts new file mode 100644 index 00000000..860650e2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts @@ -0,0 +1,182 @@ +import { FactoryProvider } from '@nestjs/common'; +import { + EntityManager, + MikroORM, + EntityRepository, + EntityMetadata, + MetadataStorage, +} from '@mikro-orm/core'; +import { camelToKebab, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { getMikroORMToken } from '@mikro-orm/nestjs'; + +import { + CURRENT_DATA_SOURCE_TOKEN, + CURRENT_ENTITY_MANAGER_TOKEN, + CURRENT_ENTITY_REPOSITORY, + FIELD_FOR_ENTITY, + GLOBAL_MODULE_OPTIONS_TOKEN, + PARAMS_FOR_ZOD_SCHEMA, + RUN_IN_TRANSACTION_FUNCTION, + ORM_SERVICE, +} from '../../../constants'; + +import { + ConfigParam, + EntityClass, + EntityName, + EntityTarget, + ObjectLiteral, + RequiredFromPartial, + ResultGeneralParam, + ResultMicroOrmModuleOptions, + RunInTransaction, +} from '../../../types'; +import { + EntityProps, + FieldWithType, + GetFieldForEntity, + ZodParams, +} from '../../mixin/types'; +import { + getField, + getPropsTreeForRepository, + getArrayPropsForEntity, + getTypeForAllProps, + getRelationTypeArray, + getTypePrimaryColumn, + getFieldWithType, + getPropsFromDb, + getRelationTypeName, + getRelationTypePrimaryColumn, +} from '../orm-helper'; + +import { getEntityName } from '../../mixin/helper'; +import { ENTITY_METADATA_TOKEN } from '../constants'; +import { MicroOrmService } from '../service'; + +export function CurrentMicroOrmProvider( + connectionName?: string +): FactoryProvider { + return { + provide: CURRENT_DATA_SOURCE_TOKEN, + useFactory: (mikroORM: MikroORM) => mikroORM, + inject: [connectionName ? getMikroORMToken(connectionName) : MikroORM], + }; +} + +export function CurrentEntityManager(): FactoryProvider { + return { + provide: CURRENT_ENTITY_MANAGER_TOKEN, + useFactory: (mikroORM: MikroORM) => mikroORM.em, + inject: [CURRENT_DATA_SOURCE_TOKEN], + }; +} + +export function CurrentEntityRepository( + entity: E +): FactoryProvider> { + return { + provide: CURRENT_ENTITY_REPOSITORY, + useFactory: (entityManager: EntityManager) => + entityManager.getRepository(entity as unknown as EntityClass), + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + }; +} + +export function CurrentEntityMetadata(): FactoryProvider { + return { + provide: ENTITY_METADATA_TOKEN, + useFactory: (mikroORM: MikroORM) => mikroORM.getMetadata(), + inject: [CURRENT_DATA_SOURCE_TOKEN], + }; +} + +export function GetFieldForEntity(): FactoryProvider< + GetFieldForEntity +> { + return { + provide: FIELD_FOR_ENTITY, + useFactory: (entityManager: EntityManager) => { + return (entity: EntityTarget) => + getField(entityManager.getMetadata().get(entity as EntityClass)); + }, + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + }; +} + +export function ZodParamsFactory( + currentEntity: EntityClass +): FactoryProvider>> { + return { + provide: PARAMS_FOR_ZOD_SCHEMA, + inject: [ENTITY_METADATA_TOKEN, GLOBAL_MODULE_OPTIONS_TOKEN], + useFactory: ( + metadataStorage: MetadataStorage, + config: ResultMicroOrmModuleOptions + ) => { + const metadata = metadataStorage.get(currentEntity); + const arrayConfig = config.options.arrayType; + + const primaryColumns = metadata.getPrimaryProp() + .name as unknown as EntityProps; + + const fieldWithType = ObjectTyped.entries( + getFieldWithType(metadata, arrayConfig) + ) + .filter(([key]) => key !== primaryColumns) + .reduce( + (acum, [key, type]) => ({ + ...acum, + [key]: type, + }), + {} as FieldWithType + ); + + return { + entityFieldsStructure: getField(metadata), + entityRelationStructure: getPropsTreeForRepository( + metadataStorage, + currentEntity + ), + propsArray: getArrayPropsForEntity( + metadataStorage, + currentEntity, + arrayConfig + ), + propsType: getTypeForAllProps( + metadataStorage, + currentEntity, + arrayConfig + ), + typeId: getTypePrimaryColumn(metadata), + typeName: camelToKebab(getEntityName(currentEntity)), + fieldWithType, + propsDb: getPropsFromDb(metadata, arrayConfig), + primaryColumn: primaryColumns, + relationArrayProps: getRelationTypeArray(metadata), + relationPopsName: getRelationTypeName(metadata), + primaryColumnType: getRelationTypePrimaryColumn( + metadataStorage, + currentEntity + ), + } satisfies ZodParams>; + }, + }; +} + +export function RunInTransactionFactory(): FactoryProvider { + return { + provide: RUN_IN_TRANSACTION_FUNCTION, + inject: [], + useFactory() { + return async (callback) => {}; + }, + }; +} + +export function OrmServiceFactory() { + return { + provide: ORM_SERVICE, + useClass: MicroOrmService, + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts index 4b7a1069..a5d7b76f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/index.ts @@ -1,2 +1,2 @@ -export * from './micro-orm.module'; +export * from './micro-orm-json-api.module'; export * from './type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts new file mode 100644 index 00000000..5e4883c9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts @@ -0,0 +1,57 @@ +import { DynamicModule } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; + +import { NestProvider, ResultModuleOptions, ObjectLiteral } from '../../types'; +import { GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; +import { + CurrentEntityManager, + CurrentEntityMetadata, + CurrentEntityRepository, + CurrentMicroOrmProvider, + GetFieldForEntity, + OrmServiceFactory, + RunInTransactionFactory, + ZodParamsFactory, +} from './factory'; + +export class MicroOrmJsonApiModule { + static module = 'microOrm' as const; + static forRoot(options: ResultModuleOptions): DynamicModule { + const optionProvider = { + provide: GLOBAL_MODULE_OPTIONS_TOKEN, + useValue: options, + }; + + const microOrmModule = MikroOrmModule.forFeature( + options.entities, + options.connectionName + ); + + const currentProvider = [ + ...(options.providers || []), + optionProvider, + CurrentMicroOrmProvider(options.connectionName), + CurrentEntityManager(), + GetFieldForEntity(), + RunInTransactionFactory(), + ]; + + const currentImport = [microOrmModule, ...(options.imports || [])]; + + return { + module: MicroOrmJsonApiModule, + imports: currentImport, + providers: currentProvider, + exports: [...currentProvider, ...currentImport], + }; + } + + static getUtilProviders(entity: ObjectLiteral): NestProvider { + return [ + CurrentEntityRepository(entity), + CurrentEntityMetadata(), + ZodParamsFactory(entity as any), + OrmServiceFactory(), + ]; + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts deleted file mode 100644 index c2b3095f..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NestProvider, ResultModuleOptions, ObjectLiteral } from '../../types'; -import { DynamicModule } from '@nestjs/common'; - -export class MicroOrmModule { - static forRoot(options: ResultModuleOptions): DynamicModule { - return { - module: MicroOrmModule, - }; - } - - static getUtilProviders(entity: ObjectLiteral): NestProvider { - return []; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts new file mode 100644 index 00000000..9326ccef --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts @@ -0,0 +1,265 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { + MetadataStorage, + EntityManager, + Collection, + MikroORM, +} from '@mikro-orm/core'; +import { + EntityProps, + EntityRelation, + ObjectTyped, +} from '@klerick/json-api-nestjs-shared'; +import { IMemoryDb } from 'pg-mem'; + +import { createAndPullSchemaBase } from '../../../mock-utils'; +import { + mockDBTestModule, + Users, + pullAllData, + pullUser, + Addresses, + Notes, + Roles, +} from '../../../mock-utils/microrom'; +import { + CurrentMicroOrmProvider, + CurrentEntityManager, + CurrentEntityMetadata, +} from '../factory'; + +import { DEFAULT_ARRAY_TYPE, ENTITY_METADATA_TOKEN } from '../constants'; + +import { + getField, + getPropsTreeForRepository, + getArrayPropsForEntity, + getArrayFields, + getFieldWithType, + getTypeForAllProps, + getRelationTypeArray, + getTypePrimaryColumn, + getPropsFromDb, + getRelationTypeName, + getRelationTypePrimaryColumn, +} from './'; +import { CURRENT_ENTITY_MANAGER_TOKEN } from '../../../constants'; + +import { ArrayPropsForEntity, PropsArray, TypeField } from '../../mixin/types'; + +describe('microorm-orm-helper', () => { + let db: IMemoryDb; + let entityMetadataToken: MetadataStorage; + let em: EntityManager; + let user: Users; + let userWithRelation: Users; + let mikroORM: MikroORM; + const config = DEFAULT_ARRAY_TYPE; + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + CurrentMicroOrmProvider(), + CurrentEntityManager(), + CurrentEntityMetadata(), + ], + }).compile(); + + entityMetadataToken = module.get(ENTITY_METADATA_TOKEN); + em = module.get(CURRENT_ENTITY_MANAGER_TOKEN); + mikroORM = module.get(MikroORM); + user = await pullUser(); + const { roles, comments, notes, ...other } = user; + user = other as Users; + user.id = 1; + + userWithRelation = await pullAllData(em); + }); + + afterAll(() => { + mikroORM.close(true); + }); + + it('getField', async () => { + const { field, relations } = getField(entityMetadataToken.get(Users)); + + const userFieldProps = Object.getOwnPropertyNames( + user + ) as EntityProps[]; + const hasUserFieldInResultField = userFieldProps.some( + (field) => !field.includes(field) + ); + + const hasResultInUserField = field.some( + (field) => !userFieldProps.includes(field) + ); + + const userRelationProps: EntityRelation[] = ( + Object.getOwnPropertyNames(userWithRelation) as (EntityProps & + EntityRelation)[] + ) + .filter((props) => !userFieldProps.includes(props)) + .filter((i) => i !== '__helper' && i !== '__gettersDefined'); + + const hasUserRelationInResultField = userRelationProps.some( + (field) => !relations.includes(field) + ); + + const hasResultInUserRelation = relations.some( + (field) => !userRelationProps.includes(field) + ); + + expect(hasUserFieldInResultField).toEqual(false); + expect(hasResultInUserField).toEqual(false); + + expect(hasUserRelationInResultField).toEqual(false); + expect(hasResultInUserRelation).toEqual(false); + }); + + it('getPropsTreeForRepository', () => { + const relationField = getPropsTreeForRepository(entityMetadataToken, Users); + const userFieldProps = Object.getOwnPropertyNames( + user + ) as EntityProps[]; + + const userRelationProps: EntityRelation[] = ( + Object.getOwnPropertyNames(userWithRelation) as (EntityProps & + EntityRelation)[] + ) + .filter((props) => !userFieldProps.includes(props)) + .filter((i) => i !== '__helper' && i !== '__gettersDefined'); + + const hasUserRelationInResultField = userRelationProps.some( + (field) => !Object.keys(relationField).includes(field) + ); + const hasResultInUserRelation = ObjectTyped.keys(relationField).some( + (field) => !userRelationProps.includes(field) + ); + expect(hasUserRelationInResultField).toEqual(false); + expect(hasResultInUserRelation).toEqual(false); + + for (const [relationName, fieldsRelation] of ObjectTyped.entries( + relationField + )) { + const check = fieldsRelation.some((field) => { + const targetItem = userWithRelation[relationName]; + const target = + targetItem instanceof Collection + ? targetItem.getItems().at(0) + : targetItem; + if (!target) return true; + + // @ts-ignore + return !ObjectTyped.keys(target).includes(field); + }); + expect(check).toEqual(false); + } + }); + + it('getArrayPropsForEntity', () => { + const result = getArrayPropsForEntity(entityMetadataToken, Users, config); + const check: ArrayPropsForEntity = { + target: { + testReal: true, + testArrayNull: true, + }, + manager: { + testReal: true, + testArrayNull: true, + }, + comments: {}, + notes: {}, + userGroup: {}, + roles: {}, + addresses: { + arrayField: true, + }, + }; + expect(result).toEqual(check); + }); + + it('getArrayFields', () => { + const result = getArrayFields(entityMetadataToken.get(Addresses), config); + expect(result).toEqual({ + arrayField: true, + } as PropsArray); + }); + + it('getFieldWithType', () => { + const result = getFieldWithType(entityMetadataToken.get(Addresses), config); + expect(result.arrayField).toBe('array'); + expect(result.state).toBe('string'); + expect(result.id).toBe('number'); + expect(result.createdAt).toBe('date'); + const result2 = getFieldWithType(entityMetadataToken.get(Users), config); + + expect(result2.isActive).toBe('boolean'); + }); + + it('getTypeForAllProps', () => { + const result = getTypeForAllProps(entityMetadataToken, Users, config); + expect(result.manager.id).toBe(TypeField.number); + expect(result.testDate).toBe(TypeField.date); + // @ts-ignore + expect(result.comments.id).toBe(TypeField.number); + // @ts-ignore + expect(result.notes.id).toBe(TypeField.string); + }); + + it('getRelationType', () => { + const result = getRelationTypeArray(entityMetadataToken.get(Users)); + expect(result.roles).toBe(true); + expect(result.comments).toBe(true); + expect(result.manager).toBe(false); + expect(result.addresses).toBe(false); + expect(result.userGroup).toBe(false); + expect(result.notes).toBe(true); + }); + + it('getTypePrimaryColumn', () => { + expect(getTypePrimaryColumn(entityMetadataToken.get(Users))).toBe( + TypeField.number + ); + expect(getTypePrimaryColumn(entityMetadataToken.get(Notes))).toBe( + TypeField.string + ); + }); + + it('getPropsFromDb', () => { + const result = getPropsFromDb(entityMetadataToken.get(Users), config); + // testReal has isNullable false but have default should be true + expect(result['testReal']).toEqual({ + type: 'real', + isArray: true, + isNullable: true, + }); + + const result2 = getPropsFromDb(entityMetadataToken.get(Roles), config); + expect(result2['key']).toEqual({ + type: 'varchar', + isArray: false, + isNullable: false, + }); + }); + + it('getRelationTypeName', () => { + const result = getRelationTypeName(entityMetadataToken.get(Users)); + expect(result.roles).toBe('Roles'); + expect(result.comments).toBe('Comments'); + expect(result.manager).toBe('Users'); + expect(result.addresses).toBe('Addresses'); + expect(result.userGroup).toBe('UserGroups'); + expect(result.notes).toBe('Notes'); + }); + + it('getRelationTypePrimaryColumn', () => { + const result = getRelationTypePrimaryColumn(entityMetadataToken, Users); + expect(result.roles).toBe(TypeField.number); + expect(result.comments).toBe(TypeField.number); + expect(result.manager).toBe(TypeField.number); + expect(result.addresses).toBe(TypeField.number); + expect(result.userGroup).toBe(TypeField.number); + expect(result.notes).toBe(TypeField.string); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts new file mode 100644 index 00000000..28ed46bc --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts @@ -0,0 +1,218 @@ +import { + EntityKey, + EntityMetadata, + EntityName, + MetadataStorage, + ReferenceKind, +} from '@mikro-orm/core'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectLiteral, ResultMicroOrmModuleOptions } from '../../../types'; +import { + AllFieldWithType, + ArrayPropsForEntity, + FieldWithType, + PropsArray, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + RelationTree, + ResultGetField, + TypeField, + TypeForId, +} from '../../mixin/types'; + +import { getEntityName } from '../../mixin/helper'; + +export const getField = ( + entityMetadata: EntityMetadata +): ResultGetField => { + const relations = entityMetadata.relations.map((i) => i.name); + + const field = entityMetadata.props + .map((i) => i.name) + .filter((i) => !relations.includes(i)); + + return { + relations, + field, + } as unknown as ResultGetField; +}; + +export const getPropsTreeForRepository = ( + metadataStorage: MetadataStorage, + entity: EntityName +): RelationTree => { + const entityMetadata = metadataStorage.get(entity); + + const relationType = entityMetadata.relations.reduce((acum, item) => { + acum[item.name] = item.entity(); + return acum; + }, {} as Record, EntityName>); + + return ObjectTyped.entries(relationType).reduce( + (acum, [key, value]) => ({ + ...acum, + ...{ [key]: getField(metadataStorage.get(value))['field'] }, + }), + {} as RelationTree + ); +}; +export const getArrayPropsForEntity = ( + metadataStorage: MetadataStorage, + entity: EntityName, + config: ResultMicroOrmModuleOptions['options']['arrayType'] +): ArrayPropsForEntity => { + const currentMetadata = metadataStorage.get(entity); + + const relationsArrayFields = currentMetadata.relations.reduce( + (acum, item) => { + const entityMetadata = metadataStorage.get(item.entity()); + acum[item.name] = getArrayFields(entityMetadata, config) as any; + return acum; + }, + {} as any + ); + + return { + target: getArrayFields(currentMetadata, config), + ...relationsArrayFields, + } as ArrayPropsForEntity; +}; + +export const getArrayFields = ( + entityMetadata: EntityMetadata, + config: ResultMicroOrmModuleOptions['options']['arrayType'] +): PropsArray => { + return ObjectTyped.entries(entityMetadata.properties).reduce( + (acum, [name, val]) => { + if (config.includes(val['type'])) { + acum[name] = true; + } + return acum; + }, + {} as Record, boolean> + ) as unknown as PropsArray; +}; + +export const getTypeForAllProps = ( + metadataStorage: MetadataStorage, + entity: EntityName, + config: ResultMicroOrmModuleOptions['options']['arrayType'] +): AllFieldWithType => { + const currentMetadata = metadataStorage.get(entity); + + const targetField = getFieldWithType(currentMetadata, config); + + const relationField = currentMetadata.relations.reduce((acum, item) => { + const entityMetadata = metadataStorage.get(item.entity()); + acum[item.name] = getFieldWithType(entityMetadata, config) as any; + return acum; + }, {} as any); + + return { + ...targetField, + ...relationField, + }; +}; + +export const getFieldWithType = ( + entityMetadata: EntityMetadata, + config: ResultMicroOrmModuleOptions['options']['arrayType'] +): FieldWithType => { + const { field } = getField(entityMetadata); + + const result = {} as any; + for (const item of field) { + const props = + entityMetadata.properties[item as unknown as EntityKey]; + + let typeProps: TypeField = TypeField.string; + if (config.includes(props['type'])) { + result[item] = TypeField.array; + continue; + } + + switch (props.runtimeType) { + case 'Date': + typeProps = TypeField.date; + break; + case 'number': + typeProps = TypeField.number; + break; + case 'boolean': + typeProps = TypeField.boolean; + break; + case 'object': + typeProps = TypeField.object; + break; + default: + typeProps = TypeField.string; + } + result[item] = typeProps; + } + + return result; +}; + +export const getRelationTypeArray = ( + entityMetadata: EntityMetadata +): RelationPropsArray => { + const typeArray = [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY]; + + const result = {} as any; + for (const item of entityMetadata.relations) { + result[item.name] = typeArray.includes(item.kind); + } + return result; +}; + +export const getTypePrimaryColumn = ( + entityMetadata: EntityMetadata +): TypeForId => { + return entityMetadata.getPrimaryProp().runtimeType === 'number' + ? TypeField.number + : TypeField.string; +}; + +export const getPropsFromDb = ( + entityMetadata: EntityMetadata, + config: ResultMicroOrmModuleOptions['options']['arrayType'] +): PropsForField => { + return getField(entityMetadata)['field'].reduce((acum, fieldName: any) => { + // @ts-ignore + const props = entityMetadata.properties[fieldName]; + const isArray = config.includes(props['type']); + let type = props.type; + if (isArray) { + type = props.columnTypes.at(0).split('[').at(0); + } + + acum[props.name] = { + type: type, + isArray: isArray, + isNullable: props.nullable || props.default !== undefined, + }; + return acum; + }, {} as any) as PropsForField; +}; + +export const getRelationTypeName = ( + entityMetadata: EntityMetadata +): RelationPropsTypeName => { + return entityMetadata.relations.reduce((acum, i) => { + acum[i.name] = getEntityName(i.entity() as any); + return acum; + }, {} as Record) as RelationPropsTypeName; +}; + +export const getRelationTypePrimaryColumn = ( + metadataStorage: MetadataStorage, + entity: EntityName +): RelationPrimaryColumnType => { + return metadataStorage.get(entity).relations.reduce((acum, item) => { + acum[item.name] = getTypePrimaryColumn(metadataStorage.get(item.entity())); + + return acum; + }, {} as Record) as RelationPrimaryColumnType; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/index.ts new file mode 100644 index 00000000..7fab7d49 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/index.ts @@ -0,0 +1 @@ +export * from './microorm-service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts new file mode 100644 index 00000000..81ed1e14 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts @@ -0,0 +1,66 @@ +import { + EntityRelation, + ResourceObject, + ResourceObjectRelationships, +} from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../types'; +import { OrmService } from '../../mixin/types'; +import { + PatchData, + PatchRelationshipData, + PostData, + PostRelationshipData, + Query, + QueryOne, +} from '../../mixin/zod'; + +export class MicroOrmService implements OrmService { + postOne(inputData: PostData): Promise> { + return {} as any; + } + patchOne( + id: number | string, + inputData: PatchData + ): Promise> { + return {} as any; + } + + getOne(id: number | string, query: QueryOne): Promise> { + return {} as any; + } + + getAll(query: Query): Promise> { + return {} as any; + } + + async deleteOne(id: number | string): Promise {} + + postRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise> { + return {} as any; + } + + getRelationship>( + id: number | string, + rel: Rel + ): Promise> { + return {} as any; + } + + patchRelationship>( + id: number | string, + rel: Rel, + input: PatchRelationshipData + ): Promise> { + return {} as any; + } + async deleteRelationship>( + id: number | string, + rel: Rel, + input: PostRelationshipData + ): Promise {} +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts index 6a557459..8e1d71b9 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/type.ts @@ -1,3 +1,3 @@ export type MicroOrmParam = { - // testParam: boolean; + arrayType?: string[]; }; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts index 72ff9d08..2d0603f7 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts @@ -7,7 +7,7 @@ import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; import { bindController } from './bind-controller'; -import { Users } from '../../../mock-utils'; +import { Users } from '../../../mock-utils/typeorm'; import { DEFAULT_CONNECTION_NAME } from '../../../constants'; import { ParseIntPipe, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts index 8ca13ac8..9060696b 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.spec.ts @@ -5,7 +5,7 @@ import { PROPERTY_DEPS_METADATA, } from '@nestjs/common/constants'; import { createController } from './create-controller'; -import { Users } from '../../../mock-utils'; +import { Users } from '../../../mock-utils/typeorm'; import { JsonBaseController } from '../controller/json-base.controller'; import { JSON_API_CONTROLLER_POSTFIX, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts index 4cba8fcf..9d5d086e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/interceptors/error.interceptors.ts @@ -23,7 +23,7 @@ import { ObjectLiteral } from '../../../types'; // PostgresErrorCode, // } from '../../helper'; // import { HttpException } from '@nestjs/common'; - +// #TODO need implement @Injectable() export class ErrorInterceptors implements NestInterceptor diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts index bd87447c..a0f88160 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts @@ -3,7 +3,7 @@ import { BadRequestException } from '@nestjs/common'; import { QueryField } from '@klerick/json-api-nestjs-shared'; import { QueryCheckSelectField } from './query-check-select-field'; -import { Users } from '../../../../mock-utils'; +import { Users } from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN } from '../../../../constants'; import { Query } from '../../zod'; import { ConfigParam, ObjectLiteral } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts index 04bdb9c4..fbfa1ce0 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import { QueryField } from '@klerick/json-api-nestjs-shared'; import { QueryFiledInIncludePipe } from './query-filed-in-include.pipe'; -import { Users } from '../../../../mock-utils'; +import { Users } from '../../../../mock-utils/typeorm'; import { Query } from '../../zod'; describe('QueryFiledInIncludePipe', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts index 6e15f6d9..a8b89a19 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-patch-schema/index.spec.ts @@ -8,67 +8,22 @@ import { TypeField, TypeForId, } from '../../types'; -import { Users } from '../../../../mock-utils'; +import { Users } from '../../../../mock-utils/typeorm'; import { zodPatch, PatchData } from './'; import { ZodError } from 'zod'; +import { + fieldTypeUsers as fieldWithType, + propsDb, + relationArrayProps, + relationPopsName, + primaryColumnType, +} from '../../../../utils/___test___/test.helper'; + const typeId: TypeForId = TypeField.number; -const typeName = 'Users'; -const fieldWithType: FieldWithType = { - id: TypeField.number, - login: TypeField.string, - firstName: TypeField.string, - testReal: TypeField.array, - testArrayNull: TypeField.array, - lastName: TypeField.string, - isActive: TypeField.boolean, - createdAt: TypeField.date, - testDate: TypeField.date, - updatedAt: TypeField.date, -}; -const propsDb: PropsForField = { - id: { type: 'number', isArray: false, isNullable: false }, - login: { type: 'string', isArray: false, isNullable: false }, - firstName: { type: 'string', isArray: false, isNullable: true }, - testReal: { type: 'number', isArray: true, isNullable: false }, - testArrayNull: { type: 'number', isArray: true, isNullable: true }, - lastName: { type: 'string', isArray: false, isNullable: true }, - isActive: { type: 'boolean', isArray: false, isNullable: true }, - createdAt: { type: 'date', isArray: false, isNullable: true }, - testDate: { type: 'date', isArray: false, isNullable: true }, - updatedAt: { type: 'date', isArray: false, isNullable: true }, - notes: { type: 'string', isArray: false, isNullable: true }, - roles: { type: 'number', isArray: true, isNullable: true }, - addresses: { type: 'number', isArray: true, isNullable: true }, - userGroup: { type: 'number', isArray: false, isNullable: true }, - manager: { type: 'number', isArray: false, isNullable: true }, - comments: { type: 'number', isArray: true, isNullable: true }, -}; + const primaryColumn: EntityProps = 'id'; -const relationArrayProps: RelationPropsArray = { - roles: true, - comments: true, - notes: true, - addresses: false, - userGroup: false, - manager: false, -}; -const relationPopsName: RelationPropsTypeName = { - roles: 'Roles', - comments: 'Comments', - notes: 'Notes', - addresses: 'Addresses', - userGroup: 'UserGroups', - manager: 'Users', -}; -const primaryColumnType: RelationPrimaryColumnType = { - roles: TypeField.number, - userGroup: TypeField.number, - manager: TypeField.number, - addresses: TypeField.number, - comments: TypeField.number, - notes: TypeField.string, -}; + const schema = zodPatch( typeId, 'users', diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts index b43feb78..70e41e5d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-post-schema/index.spec.ts @@ -1,74 +1,20 @@ -import { - EntityProps, - FieldWithType, - PropsForField, - RelationPrimaryColumnType, - RelationPropsArray, - RelationPropsTypeName, - TypeField, - TypeForId, -} from '../../types'; -import { Users } from '../../../../mock-utils'; -import { zodPost, PostData } from './'; +import { EntityProps, TypeField, TypeForId } from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; +import { zodPost } from './'; import { ZodError } from 'zod'; +import { + fieldTypeUsers as fieldWithType, + propsDb, + relationArrayProps, + relationPopsName, + primaryColumnType, +} from '../../../../utils/___test___/test.helper'; + const typeId: TypeForId = TypeField.number; -const typeName = 'Users'; -const fieldWithType: FieldWithType = { - id: TypeField.number, - login: TypeField.string, - firstName: TypeField.string, - testReal: TypeField.array, - testArrayNull: TypeField.array, - lastName: TypeField.string, - isActive: TypeField.boolean, - createdAt: TypeField.date, - testDate: TypeField.date, - updatedAt: TypeField.date, -}; -const propsDb: PropsForField = { - id: { type: 'number', isArray: false, isNullable: false }, - login: { type: 'string', isArray: false, isNullable: false }, - firstName: { type: 'string', isArray: false, isNullable: true }, - testReal: { type: 'number', isArray: true, isNullable: false }, - testArrayNull: { type: 'number', isArray: true, isNullable: true }, - lastName: { type: 'string', isArray: false, isNullable: true }, - isActive: { type: 'boolean', isArray: false, isNullable: true }, - createdAt: { type: 'date', isArray: false, isNullable: true }, - testDate: { type: 'date', isArray: false, isNullable: true }, - updatedAt: { type: 'date', isArray: false, isNullable: true }, - notes: { type: 'string', isArray: false, isNullable: true }, - roles: { type: 'number', isArray: true, isNullable: true }, - addresses: { type: 'number', isArray: true, isNullable: true }, - userGroup: { type: 'number', isArray: false, isNullable: true }, - manager: { type: 'number', isArray: false, isNullable: true }, - comments: { type: 'number', isArray: true, isNullable: true }, -}; + const primaryColumn: EntityProps = 'id'; -const relationArrayProps: RelationPropsArray = { - roles: true, - comments: true, - notes: true, - addresses: false, - userGroup: false, - manager: false, -}; -const relationPopsName: RelationPropsTypeName = { - roles: 'Roles', - comments: 'Comments', - notes: 'Notes', - addresses: 'Addresses', - userGroup: 'UserGroups', - manager: 'Users', -}; -const primaryColumnType: RelationPrimaryColumnType = { - roles: TypeField.number, - userGroup: TypeField.number, - manager: TypeField.number, - addresses: TypeField.number, - comments: TypeField.number, - notes: TypeField.string, -}; + const schema = zodPost( typeId, 'users', diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts index 3486cbae..eb999c51 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.spec.ts @@ -1,16 +1,13 @@ import { zodFieldsInputQuery } from './fields'; import { ResultGetField } from '../../types'; -import { Users } from '../../../../mock-utils'; +import { Users } from '../../../../mock-utils/typeorm'; + +import { userFieldsStructure } from '../../../../utils/___test___/test.helper'; + +const validRelationList: ResultGetField['relations'] = + userFieldsStructure['relations']; describe('zodFieldsInputQuerySchema', () => { - const validRelationList: ResultGetField['relations'] = [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', - ]; const schema = zodFieldsInputQuery(validRelationList); it('should validate successfully with a valid target and relation', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts index 919c41df..d5986891 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts @@ -1,47 +1,13 @@ import { zodFilterInputQuery } from './filter'; -import { Users } from '../../../../mock-utils'; -import { RelationTree, ResultGetField } from '../../types'; - -const userFields: ResultGetField['field'] = [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', -]; - -const userRelations: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', - ], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], -}; +import { Users } from '../../../../mock-utils/typeorm'; +import { ResultGetField } from '../../types'; + +import { + userFieldsStructure, + userRelations, +} from '../../../../utils/___test___/test.helper'; + +const userFields: ResultGetField['field'] = userFieldsStructure['field']; describe('zodFilterInputQuery', () => { it('should return transformed result with relation and target when valid data is provided', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts index 7873d08c..48a3f61d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts @@ -1,53 +1,15 @@ import { QueryField, ObjectTyped } from '@klerick/json-api-nestjs-shared'; import { zodInputQuery } from './index'; -import { - RelationTree, - ResultGetField, - TupleOfEntityRelation, -} from '../../types'; -import { Users } from '../../../../mock-utils'; +import { ResultGetField, TupleOfEntityRelation } from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; -const userFields: ResultGetField['field'] = [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', -]; +import { + userFieldsStructure, + userRelations, +} from '../../../../utils/___test___/test.helper'; -const userRelations: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', - ], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], -}; +const userFields: ResultGetField['field'] = userFieldsStructure['field']; const entityFieldsStructure = { field: userFields, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts index 1c0d88f1..704960e0 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.spec.ts @@ -1,47 +1,11 @@ import { zodFieldsQuery } from './fields'; -import { Users } from '../../../../mock-utils'; -import { RelationTree, ResultGetField } from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; -const userFields: ResultGetField['field'] = [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', -]; +import { + userFields, + userRelations, +} from '../../../../utils/___test___/test.helper'; -const userRelations: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', - ], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], -}; const schema = zodFieldsQuery(userFields, userRelations); describe('zodFieldsQuerySchema', () => { it('should validate a target field correctly', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts index 43972032..dbc637f8 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.spec.ts @@ -1,55 +1,14 @@ import { zodFilterQuery } from './filter'; -import { Users } from '../../../../mock-utils'; -import { - RelationTree, - ResultGetField, - AllFieldWithType, - TypeField, - ArrayPropsForEntity, -} from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; +import { ArrayPropsForEntity } from '../../types'; import { ZodError } from 'zod'; import { ZodFilterInputQuery } from '../zod-input-query-schema/filter'; -const userFields: ResultGetField['field'] = [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', -]; - -const userRelations: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', - ], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], -}; +import { + userFields, + userRelations, + propsType, +} from '../../../../utils/___test___/test.helper'; const propsArray: ArrayPropsForEntity = { target: { @@ -69,65 +28,6 @@ const propsArray: ArrayPropsForEntity = { roles: {}, }; -const propsType: AllFieldWithType = { - updatedAt: TypeField.date, - testDate: TypeField.date, - createdAt: TypeField.date, - isActive: TypeField.boolean, - lastName: TypeField.string, - testArrayNull: TypeField.array, - testReal: TypeField.array, - firstName: TypeField.string, - login: TypeField.string, - id: TypeField.number, - addresses: { - arrayField: TypeField.array, - country: TypeField.string, - state: TypeField.string, - city: TypeField.string, - updatedAt: TypeField.date, - createdAt: TypeField.date, - id: TypeField.number, - }, - manager: { - updatedAt: TypeField.date, - testDate: TypeField.date, - createdAt: TypeField.date, - isActive: TypeField.boolean, - lastName: TypeField.string, - testArrayNull: TypeField.array, - testReal: TypeField.array, - firstName: TypeField.string, - login: TypeField.string, - id: TypeField.number, - }, - roles: { - isDefault: TypeField.boolean, - key: TypeField.string, - name: TypeField.string, - updatedAt: TypeField.date, - createdAt: TypeField.date, - id: TypeField.number, - }, - comments: { - kind: TypeField.string, - text: TypeField.string, - updatedAt: TypeField.date, - createdAt: TypeField.date, - id: TypeField.number, - }, - notes: { - text: TypeField.string, - updatedAt: TypeField.date, - createdAt: TypeField.date, - id: TypeField.string, - }, - userGroup: { - label: TypeField.string, - id: TypeField.number, - }, -}; - const schema = zodFilterQuery( userFields, userRelations, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts index 6283395c..97d00ecd 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/include.spec.ts @@ -1,15 +1,9 @@ import { zodIncludeQuery } from './include'; -import { ResultGetField } from '../../types'; -import { Users } from '../../../../mock-utils'; - -const relationList: ResultGetField['relations'] = [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', -]; + +import { Users } from '../../../../mock-utils/typeorm'; + +import { relationList } from '../../../../utils/___test___/test.helper'; + const schema = zodIncludeQuery(relationList); describe('zodIncludeQuery', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts index 71a5eb51..e75e6e1e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts @@ -1,66 +1,15 @@ import { FilterOperand, QueryField } from '@klerick/json-api-nestjs-shared'; import { zodQuery } from './index'; -import { - AllFieldWithType, - ArrayPropsForEntity, - RelationTree, - ResultGetField, - TypeField, -} from '../../types'; -import { Users } from '../../../../mock-utils'; +import { ArrayPropsForEntity } from '../../types'; +import { Users } from '../../../../mock-utils/typeorm'; import { InputQuery } from '../zod-input-query-schema'; import { ASC } from '../../../../constants'; -const userFields: ResultGetField = { - field: [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', - ], - relations: [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', - ], -}; - -const userRelations: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', - ], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], -}; +import { + userFieldsStructure as userFields, + userRelations, + propsType, +} from '../../../../utils/___test___/test.helper'; const propsArray: ArrayPropsForEntity = { target: { @@ -80,65 +29,6 @@ const propsArray: ArrayPropsForEntity = { roles: {}, }; -const propsType: AllFieldWithType = { - updatedAt: TypeField.date, - testDate: TypeField.date, - createdAt: TypeField.date, - isActive: TypeField.boolean, - lastName: TypeField.string, - testArrayNull: TypeField.array, - testReal: TypeField.array, - firstName: TypeField.string, - login: TypeField.string, - id: TypeField.number, - addresses: { - arrayField: TypeField.array, - country: TypeField.string, - state: TypeField.string, - city: TypeField.string, - updatedAt: TypeField.date, - createdAt: TypeField.date, - id: TypeField.number, - }, - manager: { - updatedAt: TypeField.date, - testDate: TypeField.date, - createdAt: TypeField.date, - isActive: TypeField.boolean, - lastName: TypeField.string, - testArrayNull: TypeField.array, - testReal: TypeField.array, - firstName: TypeField.string, - login: TypeField.string, - id: TypeField.number, - }, - roles: { - isDefault: TypeField.boolean, - key: TypeField.string, - name: TypeField.string, - updatedAt: TypeField.date, - createdAt: TypeField.date, - id: TypeField.number, - }, - comments: { - kind: TypeField.string, - text: TypeField.string, - updatedAt: TypeField.date, - createdAt: TypeField.date, - id: TypeField.number, - }, - notes: { - text: TypeField.string, - updatedAt: TypeField.date, - createdAt: TypeField.date, - id: TypeField.string, - }, - userGroup: { - label: TypeField.string, - id: TypeField.number, - }, -}; - const schemaQuery = zodQuery( userFields, userRelations, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts index 519c931d..53ede399 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.spec.ts @@ -1,49 +1,12 @@ import { zodSortQuery } from './sort'; -import { RelationTree, ResultGetField } from '../../types'; -import { Users } from '../../../../mock-utils'; import { ASC, DESC } from '../../../../constants'; -const userFields: ResultGetField['field'] = [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', -]; +import { + userFields, + userRelations, +} from '../../../../utils/___test___/test.helper'; -const userRelations: RelationTree = { - addresses: [ - 'arrayField', - 'country', - 'state', - 'city', - 'updatedAt', - 'createdAt', - 'id', - ], - manager: [ - 'updatedAt', - 'testDate', - 'createdAt', - 'isActive', - 'lastName', - 'testArrayNull', - 'testReal', - 'firstName', - 'login', - 'id', - ], - roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], - comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], - notes: ['text', 'updatedAt', 'createdAt', 'id'], - userGroup: ['label', 'id'], -}; const schema = zodSortQuery(userFields, userRelations); describe('zodSortQuery', () => { it('should create a Zod schema with target and relations', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts index dd94a577..7011ef35 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.spec.ts @@ -1,47 +1,13 @@ -import { z, ZodError } from 'zod'; +import { ZodError } from 'zod'; import { zodAttributes, ZodAttributes, Attributes } from './attributes'; -import { Addresses, Users } from '../../../../mock-utils'; -import { FieldWithType, PropsForField, TypeField } from '../../types'; - -const fieldTypeUsers: FieldWithType = { - id: TypeField.number, - isActive: TypeField.boolean, - firstName: TypeField.string, - createdAt: TypeField.date, - lastName: TypeField.string, - login: TypeField.string, - testDate: TypeField.date, - updatedAt: TypeField.date, - testReal: TypeField.array, - testArrayNull: TypeField.array, -}; -const propsDb: PropsForField = { - id: { type: Number, isArray: false, isNullable: false }, - login: { type: 'varchar', isArray: false, isNullable: false }, - firstName: { type: 'varchar', isArray: false, isNullable: true }, - testReal: { type: 'real', isArray: true, isNullable: false }, - testArrayNull: { type: 'real', isArray: true, isNullable: true }, - lastName: { type: 'varchar', isArray: false, isNullable: true }, - isActive: { type: 'boolean', isArray: false, isNullable: true }, - createdAt: { type: 'timestamp', isArray: false, isNullable: true }, - testDate: { type: 'timestamp', isArray: false, isNullable: true }, - updatedAt: { type: 'timestamp', isArray: false, isNullable: true }, - notes: { type: 'string', isArray: false, isNullable: true }, - roles: { type: 'number', isArray: true, isNullable: true }, - addresses: { type: 'number', isArray: true, isNullable: true }, - userGroup: { type: 'number', isArray: false, isNullable: true }, - manager: { type: 'number', isArray: false, isNullable: true }, - comments: { type: 'number', isArray: true, isNullable: true }, -}; -const fieldTypeAddresses: FieldWithType = { - id: TypeField.number, - arrayField: TypeField.array, - state: TypeField.string, - city: TypeField.string, - createdAt: TypeField.date, - updatedAt: TypeField.date, - country: TypeField.string, -}; +import { Addresses, Users } from '../../../../mock-utils/typeorm'; +import { PropsForField } from '../../types'; + +import { + fieldTypeUsers, + propsDb, + fieldTypeAddresses, +} from '../../../../utils/___test___/test.helper'; describe('attributes', () => { type SchemaTypeUsers = Attributes; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts index 642a9397..3f101754 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.spec.ts @@ -1,41 +1,15 @@ import { z, ZodError } from 'zod'; import { zodRelationships, ZodRelationships } from './relationships'; -import { Users } from '../../../../mock-utils'; +import { Users } from '../../../../mock-utils/typeorm'; + import { - RelationPropsArray, - RelationPropsTypeName, - RelationPrimaryColumnType, - TypeField, -} from '../../types'; + relationArrayProps, + relationPopsName, + primaryColumnType, +} from '../../../../utils/___test___/test.helper'; describe('zodRelationships', () => { - const relationArrayProps: RelationPropsArray = { - roles: true, - userGroup: false, - notes: true, - addresses: false, - comments: true, - manager: false, - }; - const relationPopsName: RelationPropsTypeName = { - roles: 'Roles', - userGroup: 'UserGroups', - notes: 'Notes', - addresses: 'Addresses', - comments: 'Comments', - manager: 'Users', - }; - - const primaryColumnType: RelationPrimaryColumnType = { - roles: TypeField.number, - userGroup: TypeField.number, - notes: TypeField.string, - addresses: TypeField.number, - comments: TypeField.number, - manager: TypeField.number, - }; - let relationshipsSchema: ZodRelationships; describe('POST', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts index decec6dc..33346a35 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-utils.spec.ts @@ -12,12 +12,6 @@ import { TypeField } from '../types'; describe('zod-utils', () => { describe('guardIsKeyOfObject', () => { - /** - * Function Description: - * The `guardIsKeyOfObject` function acts as a type guard that ensures the given `key` - * is a valid key of the provided object `R`. If the key exists in the object, the type check passes. - * Otherwise, it throws an error. - */ it('should not throw an error if the key exists in the object', () => { const obj = { a: 1, b: 2, c: 3 }; expect(() => guardIsKeyOfObject(obj, 'a')).not.toThrow(); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts deleted file mode 100644 index 96438c0d..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/di.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CURRENT_DATA_SOURCE_TOKEN = Symbol('CURRENT_DATA_SOURCE_TOKEN'); -export const CURRENT_ENTITY_REPOSITORY = Symbol('CURRENT_ENTITY_REPOSITORY'); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts index 403001ad..b5faede6 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/constants/index.ts @@ -1,4 +1,2 @@ -export * from './di'; - export const SUB_QUERY_ALIAS_FOR_PAGINATION = 'subQueryWithLimitOffset'; export const ALIAS_FOR_PAGINATION = 'aliasForPagination'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts index 32f8835f..5dfc69ae 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts @@ -3,10 +3,6 @@ import { getDataSourceToken } from '@nestjs/typeorm'; import { camelToKebab, ObjectTyped } from '@klerick/json-api-nestjs-shared'; import { DataSource, EntityManager, Repository } from 'typeorm'; -import { - CURRENT_DATA_SOURCE_TOKEN, - CURRENT_ENTITY_REPOSITORY, -} from '../constants'; import { CURRENT_ENTITY_MANAGER_TOKEN, FIND_ONE_ROW_ENTITY, @@ -16,6 +12,8 @@ import { FIELD_FOR_ENTITY, RUN_IN_TRANSACTION_FUNCTION, GLOBAL_MODULE_OPTIONS_TOKEN, + CURRENT_DATA_SOURCE_TOKEN, + CURRENT_ENTITY_REPOSITORY, } from '../../../constants'; import { EntityProps, @@ -48,7 +46,7 @@ import { import { TypeOrmService, TypeormUtilsService } from '../service'; import { getEntityName } from '../../mixin/helper'; -import { TypeOrmModule } from '@klerick/json-api-nestjs'; +import { TypeOrmJsonApiModule } from '../type-orm-json-api.module'; import { TypeOrmParam } from '../type'; export function CurrentDataSourceProvider( @@ -69,6 +67,17 @@ export function CurrentEntityManager(): FactoryProvider { }; } +export function CurrentEntityRepository( + entity: E +): FactoryProvider> { + return { + provide: CURRENT_ENTITY_REPOSITORY, + useFactory: (entityManager: EntityManager) => + entityManager.getRepository(entity as unknown as EntityTarget), + inject: [CURRENT_ENTITY_MANAGER_TOKEN], + }; +} + export function GetFieldForEntity(): FactoryProvider< GetFieldForEntity > { @@ -82,17 +91,6 @@ export function GetFieldForEntity(): FactoryProvider< }; } -export function CurrentEntityRepository( - entity: E -): FactoryProvider> { - return { - provide: CURRENT_ENTITY_REPOSITORY, - useFactory: (entityManager: EntityManager) => - entityManager.getRepository(entity as unknown as EntityTarget), - inject: [CURRENT_ENTITY_MANAGER_TOKEN], - }; -} - export function ZodParamsFactory(): FactoryProvider< ZodParams> > { @@ -177,7 +175,7 @@ export function RunInTransactionFactory(): FactoryProvider { inject: [GLOBAL_MODULE_OPTIONS_TOKEN, CURRENT_DATA_SOURCE_TOKEN], useFactory( options: ResultGeneralParam & { - type: typeof TypeOrmModule; + type: typeof TypeOrmJsonApiModule; options: RequiredFromPartial; }, dataSource: DataSource diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts index e9cdcce4..fd6d6101 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/index.ts @@ -1,2 +1,2 @@ -export * from './type-orm.module'; +export * from './type-orm-json-api.module'; export * from './type'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts index 80541044..f462b1f0 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts @@ -11,7 +11,6 @@ import { IMemoryDb } from 'pg-mem'; import { mockDBTestModule, - createAndPullSchemaBase, pullUser, pullAllData, providerEntities, @@ -22,7 +21,7 @@ import { Comments, Roles, UserGroups, -} from '../../../mock-utils'; +} from '../../../mock-utils/typeorm'; import { getField, @@ -40,6 +39,7 @@ import { } from './'; import { PropsArray, ArrayPropsForEntity, TypeField } from '../../mixin/types'; +import { createAndPullSchemaBase } from '../../../mock-utils'; describe('type-orm-helper', () => { let userRepository: Repository; @@ -149,7 +149,7 @@ describe('type-orm-helper', () => { const checkArray = fromRelationTreeToArrayName(relationField); for (const key of relations) { - let resultKey = + const resultKey = key === 'manager' ? 'Users' : key === 'userGroup' ? 'UserGroups' : key; const relationsRepo = @@ -237,24 +237,6 @@ describe('type-orm-helper', () => { expect(getTypePrimaryColumn(userRepository)).toBe(TypeField.number); expect(getTypePrimaryColumn(notesRepository)).toBe(TypeField.string); }); - // - // it('getPrimaryColumnsForRelation', () => { - // const result = getPrimaryColumnsForRelation(userRepository); - // expect(result.roles).toBe('id'); - // expect(result.manager).toBe('id'); - // }); - // - // it('geTisArrayRelation', () => { - // const result = getIsArrayRelation(userRepository); - // expect(result).toEqual({ - // addresses: false, - // manager: false, - // roles: true, - // comments: true, - // notes: true, - // userGroup: false, - // }); - // }); it('getTypeForAllProps', () => { const result = getTypeForAllProps(userRepository); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts index 4f730de9..818aaa5d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts @@ -3,10 +3,9 @@ import { IMemoryDb } from 'pg-mem'; import { getDataSourceToken } from '@nestjs/typeorm'; import { - createAndPullSchemaBase, mockDBTestModule, providerEntities, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CurrentDataSourceProvider, CurrentEntityManager, @@ -15,7 +14,7 @@ import { } from '../../factory'; import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; -import { getRepository, pullUser, Users } from '../../../../mock-utils'; +import { getRepository, pullUser, Users } from '../../../../mock-utils/typeorm'; import { Repository } from 'typeorm'; import { CONTROL_OPTIONS_TOKEN, ORM_SERVICE } from '../../../../constants'; @@ -26,6 +25,7 @@ import { TransformDataService, TypeormUtilsService, } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; describe('deleteOne', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts index 5a83d3fc..4f729db2 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts @@ -6,7 +6,6 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, - createAndPullSchemaBase, getRepository, mockDBTestModule, Notes, @@ -15,7 +14,7 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, @@ -35,6 +34,7 @@ import { TransformDataService, TypeormUtilsService, } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; describe('deleteRelationship', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts index 45500dc3..4efaebde 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts @@ -7,7 +7,6 @@ import { IMemoryDb } from 'pg-mem'; import { Addresses, Comments, - createAndPullSchemaBase, getRepository, mockDBTestModule, Notes, @@ -16,7 +15,7 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CurrentDataSourceProvider, CurrentEntityManager, @@ -40,6 +39,7 @@ import { TransformDataService, TypeormUtilsService, } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; function getDefaultQuery() { const filter = { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts index d0a6adaa..90dec8a8 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts @@ -8,7 +8,6 @@ import { ObjectLiteral as Entity } from '../../../../types'; import { Addresses, Comments, - createAndPullSchemaBase, getRepository, mockDBTestModule, Notes, @@ -17,7 +16,7 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, @@ -40,6 +39,7 @@ import { TransformDataService, TypeormUtilsService, } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; function getDefaultQuery() { const defaultQuery: Query = { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts index 161d1f83..11dd1bdc 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts @@ -6,7 +6,6 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, - createAndPullSchemaBase, getRepository, mockDBTestModule, Notes, @@ -15,7 +14,7 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, @@ -36,6 +35,7 @@ import { TransformDataService, TypeormUtilsService, } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; describe('getRelationship', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts index 9cc08057..30944561 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts @@ -6,7 +6,6 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, - createAndPullSchemaBase, getRepository, mockDBTestModule, Notes, @@ -15,7 +14,7 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CurrentDataSourceProvider, CurrentEntityManager, @@ -35,6 +34,7 @@ import { TransformDataService, TypeormUtilsService, } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; describe('patchOne', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts index 2cfe5343..d44ed6dc 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts @@ -6,7 +6,6 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, - createAndPullSchemaBase, getRepository, mockDBTestModule, Notes, @@ -15,7 +14,7 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, @@ -34,6 +33,7 @@ import { TransformDataService, TypeormUtilsService, } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; describe('patchRelationship', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts index a2e45206..8e7b223d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts @@ -6,7 +6,6 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, - createAndPullSchemaBase, getRepository, mockDBTestModule, Notes, @@ -16,7 +15,7 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, DEFAULT_CONNECTION_NAME, @@ -36,6 +35,7 @@ import { TransformDataService, TypeormUtilsService, } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; describe('postOne', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts index a1126f32..70a647e5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts @@ -6,7 +6,6 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, - createAndPullSchemaBase, getRepository, mockDBTestModule, Notes, @@ -15,7 +14,7 @@ import { Roles, UserGroups, Users, -} from '../../../../mock-utils'; +} from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, @@ -34,6 +33,7 @@ import { TransformDataService, TypeormUtilsService, } from '../../service'; +import { createAndPullSchemaBase } from '../../../../mock-utils'; describe('postRelationship', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts index 981d2db4..d62de08e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts @@ -3,15 +3,15 @@ import { IMemoryDb } from 'pg-mem'; import { getDataSourceToken } from '@nestjs/typeorm'; import { - createAndPullSchemaBase, mockDBTestModule, providerEntities, UserGroups, Users, -} from '../../../mock-utils'; +} from '../../../mock-utils/typeorm'; import { CurrentDataSourceProvider } from '../factory'; import { DEFAULT_CONNECTION_NAME } from '../../../constants'; import { EntityPropsMapService } from './entity-props-map.service'; +import { createAndPullSchemaBase } from '../../../mock-utils'; describe('EntityPropsMapService', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts index 160f2028..db0455f5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts @@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { DataSource, EntityTarget } from 'typeorm'; import { EntityRelation } from '@klerick/json-api-nestjs-shared'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../constants'; import { ObjectLiteral as Entity } from '../../../types'; import { ResultGetField, @@ -10,6 +9,7 @@ import { TupleOfEntityRelation, } from '../../mixin/types'; import { getField } from '../orm-helper'; +import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; @Injectable() export class EntityPropsMapService { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts index fed6e89a..ce1b9871 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts @@ -6,7 +6,6 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, - createAndPullSchemaBase, getRepository, mockDBTestModule, Notes, @@ -15,13 +14,14 @@ import { Roles, UserGroups, Users, -} from '../../../mock-utils'; +} from '../../../mock-utils/typeorm'; import { CurrentDataSourceProvider, CurrentEntityRepository } from '../factory'; import { DEFAULT_CONNECTION_NAME } from '../../../constants'; import { TransformDataService } from './transform-data.service'; import { ApplicationConfig } from '@nestjs/core'; import { VersioningType } from '@nestjs/common'; import { EntityPropsMapService } from '../service'; +import { createAndPullSchemaBase } from '../../../mock-utils'; describe('TransformDataService', () => { let db: IMemoryDb; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts index 2e80c4f2..37b91c3e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts @@ -31,8 +31,10 @@ import { import { TypeormUtilsService } from './typeorm-utils.service'; import { TransformDataService } from './transform-data.service'; -import { CONTROL_OPTIONS_TOKEN } from '../../../constants'; -import { CURRENT_ENTITY_REPOSITORY } from '../constants'; +import { + CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY_REPOSITORY, +} from '../../../constants'; import { TypeOrmParam } from '../type'; export class TypeOrmService implements OrmService { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts index 9c32607f..159bfdc5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts @@ -4,7 +4,6 @@ import { getDataSourceToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { - createAndPullSchemaBase, mockDBTestModule, providerEntities, UserGroups, @@ -15,14 +14,16 @@ import { Notes, getRepository, pullAllData, -} from '../../../mock-utils'; +} from '../../../mock-utils/typeorm'; import { CurrentDataSourceProvider, CurrentEntityManager, CurrentEntityRepository, } from '../factory'; -import { CURRENT_ENTITY_REPOSITORY } from '../constants'; -import { DEFAULT_CONNECTION_NAME } from '../../../constants'; +import { + CURRENT_ENTITY_REPOSITORY, + DEFAULT_CONNECTION_NAME, +} from '../../../constants'; import { TypeormUtilsService } from './typeorm-utils.service'; import { PostData, PostRelationshipData, Query } from '../../mixin/zod'; import { QueryField, FilterOperand } from '@klerick/json-api-nestjs-shared'; @@ -36,6 +37,7 @@ import { NotFoundException, UnprocessableEntityException, } from '@nestjs/common'; +import { createAndPullSchemaBase } from '../../../mock-utils'; function getDefaultQuery() { const filter = { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts index 9683bd32..abaec387 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts @@ -25,9 +25,9 @@ import { } from '../../../types'; import { PatchData, PostData, Query } from '../../mixin/zod'; -import { CURRENT_ENTITY_REPOSITORY } from '../constants'; import { TupleOfEntityRelation, EntityRelation } from '../../mixin/types'; import { getEntityName } from '../../mixin/helper'; +import { CURRENT_ENTITY_REPOSITORY } from '../../../constants'; type RelationAlias = { [K in TupleOfEntityRelation[number]]: string; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts similarity index 88% rename from libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts rename to libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts index 715d1c86..5413a6d1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm.module.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts @@ -1,5 +1,5 @@ import { DynamicModule } from '@nestjs/common'; -import { TypeOrmModule as MainTypeOrmModule } from '@nestjs/typeorm'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; import { NestProvider, ObjectLiteral, ResultModuleOptions } from '../../types'; @@ -21,14 +21,15 @@ import { } from './service'; import { GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; -export class TypeOrmModule { +export class TypeOrmJsonApiModule { + static module = 'typeOrm' as const; static forRoot(options: ResultModuleOptions): DynamicModule { const optionProvider = { provide: GLOBAL_MODULE_OPTIONS_TOKEN, useValue: options, }; - const typeOrmModule = MainTypeOrmModule.forFeature( + const typeOrmModule = TypeOrmModule.forFeature( options.entities as EntityClassOrSchema[], options.connectionName ); @@ -46,7 +47,7 @@ export class TypeOrmModule { const currentImport = [typeOrmModule, ...(options.imports || [])]; return { - module: TypeOrmModule, + module: TypeOrmJsonApiModule, imports: currentImport, providers: currentProvider, exports: [...currentProvider, ...currentImport], diff --git a/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts b/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts index 8c83dd54..731f4131 100644 --- a/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts +++ b/libs/json-api/json-api-nestjs/src/lib/types/module-common.types.ts @@ -1,6 +1,6 @@ import { - MicroOrmModule, - TypeOrmModule, + MicroOrmJsonApiModule, + TypeOrmJsonApiModule, TypeOrmParam, MicroOrmParam, } from '../modules'; @@ -8,22 +8,27 @@ import { import { ConfigParam, GeneralParam, ResultGeneralParam } from './config-param'; import { RequiredFromPartial } from './util-types'; -export type ModuleOptions = - | (GeneralParam & { - type: typeof MicroOrmModule; - options: Partial; - }) - | (GeneralParam & { - type?: typeof TypeOrmModule; - options: Partial; - }); +export type TypeOrmConfigParam = ConfigParam & TypeOrmParam; + +export type TypeOrmDefaultOptions = GeneralParam & { + options: Partial; +}; +export type TypeOrmOptions = GeneralParam & { + options: Partial; +}; + +export type MicroOrmConfigParam = ConfigParam & MicroOrmParam; +export type MicroOrmOptions = GeneralParam & { + options: Partial; +}; + +export type ResultTypeOrmModuleOptions = ResultGeneralParam & { + type: typeof TypeOrmJsonApiModule; +} & TypeOrmOptions & { options: RequiredFromPartial }; +export type ResultMicroOrmModuleOptions = ResultGeneralParam & { + type: typeof MicroOrmJsonApiModule; +} & MicroOrmOptions & { options: RequiredFromPartial }; export type ResultModuleOptions = - | (ResultGeneralParam & { - type: typeof MicroOrmModule; - options: RequiredFromPartial; - }) - | (ResultGeneralParam & { - type: typeof TypeOrmModule; - options: RequiredFromPartial; - }); + | ResultTypeOrmModuleOptions + | ResultMicroOrmModuleOptions; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/___test___/test.helper.ts b/libs/json-api/json-api-nestjs/src/lib/utils/___test___/test.helper.ts new file mode 100644 index 00000000..79477361 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/___test___/test.helper.ts @@ -0,0 +1,211 @@ +// @ts-nocheck +import { + AllFieldWithType, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + RelationTree, + ResultGetField, + TypeField, +} from '../../modules/mixin/types'; +import { Addresses, Users } from '../../mock-utils/typeorm'; + +export const fieldTypeUsers: FieldWithType = { + id: TypeField.number, + isActive: TypeField.boolean, + firstName: TypeField.string, + createdAt: TypeField.date, + lastName: TypeField.string, + login: TypeField.string, + testDate: TypeField.date, + updatedAt: TypeField.date, + testReal: TypeField.array, + testArrayNull: TypeField.array, +}; +export const propsDb: PropsForField = { + id: { type: Number, isArray: false, isNullable: false }, + login: { type: 'varchar', isArray: false, isNullable: false }, + firstName: { type: 'varchar', isArray: false, isNullable: true }, + testReal: { type: 'real', isArray: true, isNullable: false }, + testArrayNull: { type: 'real', isArray: true, isNullable: true }, + lastName: { type: 'varchar', isArray: false, isNullable: true }, + isActive: { type: 'boolean', isArray: false, isNullable: true }, + createdAt: { type: 'timestamp', isArray: false, isNullable: true }, + testDate: { type: 'timestamp', isArray: false, isNullable: true }, + updatedAt: { type: 'timestamp', isArray: false, isNullable: true }, + notes: { type: 'string', isArray: false, isNullable: true }, + roles: { type: 'number', isArray: true, isNullable: true }, + addresses: { type: 'number', isArray: true, isNullable: true }, + userGroup: { type: 'number', isArray: false, isNullable: true }, + manager: { type: 'number', isArray: false, isNullable: true }, + comments: { type: 'number', isArray: true, isNullable: true }, +}; +export const fieldTypeAddresses: FieldWithType = { + id: TypeField.number, + arrayField: TypeField.array, + state: TypeField.string, + city: TypeField.string, + createdAt: TypeField.date, + updatedAt: TypeField.date, + country: TypeField.string, +}; + +export const relationArrayProps: RelationPropsArray = { + roles: true, + userGroup: false, + notes: true, + addresses: false, + comments: true, + manager: false, +}; +export const relationPopsName: RelationPropsTypeName = { + roles: 'Roles', + userGroup: 'UserGroups', + notes: 'Notes', + addresses: 'Addresses', + comments: 'Comments', + manager: 'Users', +}; + +export const primaryColumnType: RelationPrimaryColumnType = { + roles: TypeField.number, + userGroup: TypeField.number, + notes: TypeField.string, + addresses: TypeField.number, + comments: TypeField.number, + manager: TypeField.number, +}; + +export const userFields: ResultGetField['field'] = [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', +]; + +export const userRelations: RelationTree = { + addresses: [ + 'arrayField', + 'country', + 'state', + 'city', + 'updatedAt', + 'createdAt', + 'id', + ], + manager: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + roles: ['isDefault', 'key', 'name', 'updatedAt', 'createdAt', 'id'], + comments: ['kind', 'text', 'updatedAt', 'createdAt', 'id'], + notes: ['text', 'updatedAt', 'createdAt', 'id'], + userGroup: ['label', 'id'], +}; + +export const propsType: AllFieldWithType = { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + addresses: { + arrayField: TypeField.array, + country: TypeField.string, + state: TypeField.string, + city: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + manager: { + updatedAt: TypeField.date, + testDate: TypeField.date, + createdAt: TypeField.date, + isActive: TypeField.boolean, + lastName: TypeField.string, + testArrayNull: TypeField.array, + testReal: TypeField.array, + firstName: TypeField.string, + login: TypeField.string, + id: TypeField.number, + }, + roles: { + isDefault: TypeField.boolean, + key: TypeField.string, + name: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + comments: { + kind: TypeField.string, + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.number, + }, + notes: { + text: TypeField.string, + updatedAt: TypeField.date, + createdAt: TypeField.date, + id: TypeField.string, + }, + userGroup: { + label: TypeField.string, + id: TypeField.number, + }, +}; + +export const relationList: ResultGetField['relations'] = [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', +]; + +export const userFieldsStructure: ResultGetField = { + field: [ + 'updatedAt', + 'testDate', + 'createdAt', + 'isActive', + 'lastName', + 'testArrayNull', + 'testReal', + 'firstName', + 'login', + 'id', + ], + relations: [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ], +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts index ca67bf72..767f90a8 100644 --- a/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/utils/helper.spec.ts @@ -1,7 +1,15 @@ import { DynamicModule, ParseIntPipe } from '@nestjs/common'; -import { MicroOrmModule, TypeOrmModule, TypeOrmParam } from '../modules'; -import { ConfigParam, RequiredFromPartial } from '../types'; +import { + MicroOrmJsonApiModule, + TypeOrmJsonApiModule, + TypeOrmParam, +} from '../modules'; +import { + ConfigParam, + RequiredFromPartial, + ResultModuleOptions, +} from '../types'; import { prepareConfig, createMixinModule } from './helper'; import { DEFAULT_CONNECTION_NAME, @@ -15,32 +23,35 @@ class A {} describe('Helper tests', () => { describe('prepareConfig', () => { it('should return default config when type is undefined', () => { - const result = prepareConfig({ - entities: [A], - options: { debug: false, requiredSelectField: false }, - }); + const result = prepareConfig( + { + entities: [A], + options: { debug: false, requiredSelectField: false }, + }, + 'typeOrm' + ); expect(Array.isArray(result.imports)).toBe(true); expect(Array.isArray(result.controllers)).toBe(true); expect(Array.isArray(result.providers)).toBe(true); - expect(result.type).toBe(TypeOrmModule); expect(result.options.debug).toBe(false); expect(result.options.requiredSelectField).toBe(false); expect(result.connectionName).toBe(DEFAULT_CONNECTION_NAME); }); it('should return TypeOrm config when type is TypeOrmModule', () => { - const result = prepareConfig({ - entities: [A], - type: TypeOrmModule, - options: { - debug: true, - requiredSelectField: true, - useSoftDelete: true, + const result = prepareConfig( + { + entities: [A], + options: { + debug: true, + requiredSelectField: true, + useSoftDelete: true, + }, }, - }); + 'typeOrm' + ); - expect(result.type).toBe(TypeOrmModule); expect(result.options.debug).toBe(true); expect( (result.options as RequiredFromPartial) @@ -49,13 +60,14 @@ describe('Helper tests', () => { }); it('should return MicroOrm config when type is MicroOrmModule', () => { - const result = prepareConfig({ - entities: [A], - type: MicroOrmModule, - options: { debug: true, requiredSelectField: true }, - }); + const result = prepareConfig( + { + entities: [A], + options: { debug: true, requiredSelectField: true }, + }, + 'microOrm' + ); - expect(result.type).toBe(MicroOrmModule); expect(result.options.debug).toBe(true); expect(result.options.requiredSelectField).toBe(true); @@ -64,10 +76,13 @@ describe('Helper tests', () => { }); it('should use default values for pipeForId, operationUrl, and overrideRoute when not provided', () => { - const result = prepareConfig({ - entities: [A], - options: {}, - }); + const result = prepareConfig( + { + entities: [A], + options: {}, + }, + 'typeOrm' + ); expect(result.options.pipeForId).toBe(ParseIntPipe); expect(result.options.operationUrl).toBe(false); @@ -81,21 +96,26 @@ describe('Helper tests', () => { @JsonApi(TestEntity) class TestController extends JsonBaseController {} const commonOrmModule = {} as DynamicModule; - const resultOptions = prepareConfig({ - entities: [TestEntity], - controllers: [TestController], - connectionName: DEFAULT_CONNECTION_NAME, - type: TypeOrmModule, - options: { - debug: true, - requiredSelectField: true, - useSoftDelete: true, + const resultOptions = prepareConfig( + { + entities: [TestEntity], + controllers: [TestController], + connectionName: DEFAULT_CONNECTION_NAME, + options: { + debug: true, + requiredSelectField: true, + useSoftDelete: true, + }, }, - }); + 'typeOrm' + ); const result = createMixinModule( TestEntity, - resultOptions, + { + ...resultOptions, + type: TypeOrmJsonApiModule, + } as ResultModuleOptions, commonOrmModule ); @@ -107,17 +127,23 @@ describe('Helper tests', () => { it('should use undefined as controller if none match the entity', () => { class TestEntity {} const commonOrmModule = {} as DynamicModule; - const resultOptions = prepareConfig({ - entities: [TestEntity], - controllers: [], - connectionName: 'test_connection', - options: { debug: false }, - imports: [], - }); + const resultOptions = prepareConfig( + { + entities: [TestEntity], + controllers: [], + connectionName: 'test_connection', + options: { debug: false }, + imports: [], + }, + 'typeOrm' + ); const result = createMixinModule( TestEntity, - resultOptions, + { + ...resultOptions, + type: TypeOrmJsonApiModule, + } as ResultModuleOptions, commonOrmModule ); @@ -133,17 +159,23 @@ describe('Helper tests', () => { const commonOrmModule = {} as DynamicModule; const importTest = { module: SharedModule }; - const resultOptions = prepareConfig({ - entities: [AnotherEntity], - controllers: [], - connectionName: 'default_connection', - options: { debug: true, useSoftDelete: true }, - imports: [importTest], - }); + const resultOptions = prepareConfig( + { + entities: [AnotherEntity], + controllers: [], + connectionName: 'default_connection', + options: { debug: true, useSoftDelete: true }, + imports: [importTest], + }, + 'typeOrm' + ); const result = createMixinModule( AnotherEntity, - resultOptions, + { + ...resultOptions, + type: TypeOrmJsonApiModule, + } as ResultModuleOptions, commonOrmModule ); expect(result.imports?.at(1)).toEqual(importTest); diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts b/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts index aa2154d5..7c7f1bd6 100644 --- a/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts +++ b/libs/json-api/json-api-nestjs/src/lib/utils/helper.ts @@ -4,34 +4,33 @@ import { AnyEntity, ConfigParam, EntityName, - ModuleOptions, + MicroOrmOptions, RequiredFromPartial, ResultModuleOptions, + TypeOrmConfigParam, + MicroOrmConfigParam, + TypeOrmOptions, } from '../types'; import { DEFAULT_CONNECTION_NAME, JSON_API_DECORATOR_ENTITY, } from '../constants'; -import { - MicroOrmParam, - TypeOrmParam, - MicroOrmModule, - TypeOrmModule, - AtomicOperationModule, -} from '../modules'; +import { TypeOrmParam, AtomicOperationModule, MicroOrmParam } from '../modules'; import { MixinModule } from '../modules/mixin/mixin.module'; import { Type } from '@nestjs/common/interfaces'; import { RouterModule } from '@nestjs/core'; +import { DEFAULT_ARRAY_TYPE } from '../modules/micro-orm/constants'; export function prepareConfig( - moduleOptions: ModuleOptions -): ResultModuleOptions { + moduleOptions: TypeOrmOptions | MicroOrmOptions, + type: 'typeOrm' | 'microOrm' +): Omit { const { options: inputOptions } = moduleOptions; let resulOptions: - | RequiredFromPartial - | RequiredFromPartial; - let resulType: typeof TypeOrmModule | typeof MicroOrmModule; + | RequiredFromPartial + | RequiredFromPartial; + const configParam: RequiredFromPartial = { debug: !!inputOptions.debug, requiredSelectField: !!inputOptions.requiredSelectField, @@ -40,34 +39,37 @@ export function prepareConfig( pipeForId: inputOptions.pipeForId || ParseIntPipe, }; - moduleOptions.type = moduleOptions.type || TypeOrmModule; - - if (moduleOptions.type === TypeOrmModule) { + if (type === 'typeOrm') { const { runInTransaction, useSoftDelete } = moduleOptions.options as Partial; - resulType = TypeOrmModule; resulOptions = { ...configParam, useSoftDelete: useSoftDelete ? useSoftDelete : false, runInTransaction: runInTransaction ? runInTransaction : false, - } as ConfigParam & RequiredFromPartial; + }; } else { - resulType = MicroOrmModule; + const { arrayType } = moduleOptions.options as Partial< + ConfigParam & MicroOrmParam + >; + resulOptions = { ...configParam, + arrayType: [...DEFAULT_ARRAY_TYPE, ...(arrayType || [])], }; } return { - connectionName: moduleOptions.connectionName || DEFAULT_CONNECTION_NAME, + connectionName: + type === 'typeOrm' + ? moduleOptions.connectionName || DEFAULT_CONNECTION_NAME + : (moduleOptions.connectionName as any), entities: moduleOptions.entities, imports: moduleOptions.imports || [], providers: moduleOptions.providers || [], controllers: moduleOptions.controllers || [], - type: resulType, - options: resulOptions, - } satisfies ResultModuleOptions; + options: resulOptions as any, + } satisfies Omit; } export function createMixinModule( diff --git a/libs/database/.eslintrc.json b/libs/microorm-database/.eslintrc.json similarity index 85% rename from libs/database/.eslintrc.json rename to libs/microorm-database/.eslintrc.json index 9d9c0db5..8d4e2111 100644 --- a/libs/database/.eslintrc.json +++ b/libs/microorm-database/.eslintrc.json @@ -1,5 +1,5 @@ { - "extends": ["../../.eslintrc.json"], + "extends": ["../../.eslintrc.base.json"], "ignorePatterns": ["!**/*"], "overrides": [ { diff --git a/libs/microorm-database/README.md b/libs/microorm-database/README.md new file mode 100644 index 00000000..3011b956 --- /dev/null +++ b/libs/microorm-database/README.md @@ -0,0 +1,7 @@ +# microorm-database + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test microorm-database` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/microorm-database/jest.config.ts b/libs/microorm-database/jest.config.ts new file mode 100644 index 00000000..2f7137c0 --- /dev/null +++ b/libs/microorm-database/jest.config.ts @@ -0,0 +1,10 @@ +export default { + displayName: 'microorm-database', + preset: '../../jest.preset.js', + testEnvironment: 'node', + transform: { + '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], + }, + moduleFileExtensions: ['ts', 'js', 'html'], + coverageDirectory: '../../coverage/libs/microorm-database', +}; diff --git a/libs/microorm-database/project.json b/libs/microorm-database/project.json new file mode 100644 index 00000000..b72c2185 --- /dev/null +++ b/libs/microorm-database/project.json @@ -0,0 +1,9 @@ +{ + "name": "microorm-database", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/microorm-database/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project microorm-database --web", + "targets": {} +} diff --git a/libs/microorm-database/src/index.ts b/libs/microorm-database/src/index.ts new file mode 100644 index 00000000..84924fb7 --- /dev/null +++ b/libs/microorm-database/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/micro-orm-database.module'; +export * from './lib/entities'; diff --git a/libs/microorm-database/src/lib/config-cli.ts b/libs/microorm-database/src/lib/config-cli.ts new file mode 100644 index 00000000..d4520b98 --- /dev/null +++ b/libs/microorm-database/src/lib/config-cli.ts @@ -0,0 +1,45 @@ +import { Options as PgOptions, PostgreSqlDriver } from '@mikro-orm/postgresql'; +import { Options as MyOptions, MySqlDriver } from '@mikro-orm/mysql'; +import { TSMigrationGenerator } from '@mikro-orm/migrations'; +import { Options } from '@mikro-orm/core'; +import { join } from 'path'; + +import * as allEntities from './entities'; + +const entitiesArray = Object.values(allEntities).filter( + (maybeClass) => typeof maybeClass === 'function' +); + +const mySqlOptions: MyOptions = { + driver: MySqlDriver, +}; + +const pgSqlOptions: PgOptions = { + driver: PostgreSqlDriver, +}; + +const config: Options = { + // dbName: process.env['DB_NAME'], + dbName: 'microorm-test', + host: process.env['DB_HOST'], + port: parseInt(`${process.env['DB_PORT']}`, 10), + user: process.env['DB_USERNAME'], + password: process.env['DB_PASSWORD'], + entitiesTs: [join(__dirname, '/entities/**/*')], + entities: entitiesArray, + debug: process.env['DB_LOGGING'] === '1', + ...(process.env['DB_TYPE'] === 'mysql' ? mySqlOptions : pgSqlOptions), + migrations: { + tableName: 'migrations', + path: join(__dirname, '/migrations'), + glob: '!(*.d).{js,ts}', + transactional: false, + allOrNothing: true, + dropTables: true, + snapshot: true, + emit: 'ts', + generator: TSMigrationGenerator, + }, +}; + +export default config; diff --git a/libs/microorm-database/src/lib/config.ts b/libs/microorm-database/src/lib/config.ts new file mode 100644 index 00000000..cbdc17cb --- /dev/null +++ b/libs/microorm-database/src/lib/config.ts @@ -0,0 +1,10 @@ +import { Options } from '@mikro-orm/core'; + +import ormConfig from './config-cli'; + +const { entitiesTs, ...configOther } = ormConfig; + +export const config: Options = { + discovery: { requireEntitiesArray: true }, + ...configOther, +}; diff --git a/libs/microorm-database/src/lib/entities/addresses.ts b/libs/microorm-database/src/lib/entities/addresses.ts new file mode 100644 index 00000000..2ebf6446 --- /dev/null +++ b/libs/microorm-database/src/lib/entities/addresses.ts @@ -0,0 +1,58 @@ +import { Entity, PrimaryKey, Property, OneToOne } from '@mikro-orm/core'; + +import { Users, IUsers } from '.'; + +export type IAddresses = Addresses; + +@Entity({ + tableName: 'addresses', +}) +export class Addresses { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + columnType: 'varchar', + length: 70, + nullable: true, + }) + public city!: string; + + @Property({ + columnType: 'varchar', + length: 70, + nullable: true, + }) + public state!: string; + + @Property({ + columnType: 'varchar', + length: 68, + nullable: true, + }) + public country!: string; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @OneToOne(() => Users, (item) => item.addresses) + public user!: IUsers; +} diff --git a/libs/microorm-database/src/lib/entities/book-list.ts b/libs/microorm-database/src/lib/entities/book-list.ts new file mode 100644 index 00000000..041059e3 --- /dev/null +++ b/libs/microorm-database/src/lib/entities/book-list.ts @@ -0,0 +1,51 @@ +import { + Entity, + PrimaryKey, + Property, + ManyToMany, + Collection, +} from '@mikro-orm/core'; + +import { IUsers, Users } from './users'; + +export type IBookList = BookList; + +@Entity({ + tableName: 'book_list', +}) +export class BookList { + @PrimaryKey({ + type: 'uuid', + defaultRaw: 'uuid_generate_v4()', + }) + public id!: string; + + @Property({ + type: 'text', + nullable: false, + }) + public text!: string; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + type: 'timestamp', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToMany(() => Users, (item) => item.books) + public users = new Collection(this); +} diff --git a/libs/microorm-database/src/lib/entities/comments.ts b/libs/microorm-database/src/lib/entities/comments.ts new file mode 100644 index 00000000..2b354d6d --- /dev/null +++ b/libs/microorm-database/src/lib/entities/comments.ts @@ -0,0 +1,53 @@ +import { Entity, PrimaryKey, Property, Enum, ManyToOne } from '@mikro-orm/core'; + +export enum CommentKind { + Comment = 'COMMENT', + Message = 'MESSAGE', + Note = 'NOTE', +} + +import { Users, IUsers } from '.'; + +export type IComments = Comments; + +@Entity({ + tableName: 'comments', +}) +export class Comments { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + columnType: 'text', + }) + public text!: string; + + @Enum({ items: () => CommentKind, nativeEnumName: 'comment_kind_enum' }) + public kind!: CommentKind; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToOne(() => Users, { + fieldName: 'created_by', + }) + createdBy!: IUsers; +} diff --git a/libs/microorm-database/src/lib/entities/index.ts b/libs/microorm-database/src/lib/entities/index.ts new file mode 100644 index 00000000..cd841496 --- /dev/null +++ b/libs/microorm-database/src/lib/entities/index.ts @@ -0,0 +1,5 @@ +export * from './users'; +export * from './roles'; +export * from './addresses'; +export * from './comments'; +export * from './book-list'; diff --git a/libs/microorm-database/src/lib/entities/roles.ts b/libs/microorm-database/src/lib/entities/roles.ts new file mode 100644 index 00000000..3dcd026d --- /dev/null +++ b/libs/microorm-database/src/lib/entities/roles.ts @@ -0,0 +1,67 @@ +import { + Entity, + PrimaryKey, + Property, + ManyToMany, + Collection, +} from '@mikro-orm/core'; + +import { Users, IUsers } from '.'; + +export type IRoles = Roles; + +@Entity({ + tableName: 'roles', +}) +export class Roles { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'varchar', + length: 128, + nullable: true, + default: 'NULL', + }) + public name!: string; + + @Property({ + type: 'varchar', + length: 128, + nullable: false, + unique: true, + }) + public key!: string; + + @Property({ + name: 'is_default', + type: 'boolean', + default: false, + }) + public isDefault!: boolean; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + type: 'timestamp', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToMany(() => Users, (item) => item.roles) + public users = new Collection(this); +} diff --git a/libs/microorm-database/src/lib/entities/users.ts b/libs/microorm-database/src/lib/entities/users.ts new file mode 100644 index 00000000..c8fed805 --- /dev/null +++ b/libs/microorm-database/src/lib/entities/users.ts @@ -0,0 +1,105 @@ +import { + Entity, + PrimaryKey, + Property, + ManyToMany, + OneToOne, + Collection, + OneToMany, +} from '@mikro-orm/core'; + +import { Roles, Addresses, IAddresses, Comments, BookList } from './'; + +export type IUsers = Users; + +@Entity({ + tableName: 'users', +}) +export class Users { + @PrimaryKey({ + autoincrement: true, + }) + public id!: number; + + @Property({ + type: 'varchar', + length: 100, + nullable: false, + unique: true, + }) + public login!: string; + + @Property({ + name: 'first_name', + type: 'varchar', + length: 100, + nullable: true, + default: 'NULL', + }) + public firstName!: string; + + @Property({ + name: 'last_name', + type: 'varchar', + length: 100, + nullable: true, + default: 'NULL', + }) + public lastName!: string; + + @Property({ + name: 'is_active', + type: 'boolean', + nullable: true, + default: false, + }) + public isActive!: boolean; + + @Property({ + length: 0, + name: 'created_at', + nullable: true, + defaultRaw: 'CURRENT_TIMESTAMP(0)', + columnType: 'timestamp(0) without time zone', + type: 'timestamp', + }) + createdAt: Date = new Date(); + + @Property({ + length: 0, + onUpdate: () => new Date(), + name: 'updated_at', + nullable: true, + columnType: 'timestamp(0) without time zone', + defaultRaw: 'CURRENT_TIMESTAMP(0)', + }) + updatedAt: Date = new Date(); + + @ManyToMany(() => Roles, (role) => role.users, { + owner: true, + pivotTable: 'users_have_roles', + }) + public roles = new Collection(this); + + @OneToOne(() => Addresses, { + owner: true, + fieldName: 'addresses_id', + }) + public addresses!: IAddresses; + + @OneToOne(() => Users, { + owner: true, + nullable: true, + fieldName: 'manager_id', + }) + public manager!: IUsers; + + @OneToMany(() => Comments, (comment) => comment.createdBy) + comments = new Collection(this); + + @ManyToMany(() => BookList, (item) => item.users, { + owner: true, + pivotTable: 'users_have_book', + }) + public books = new Collection(this); +} diff --git a/libs/microorm-database/src/lib/micro-orm-database.module.ts b/libs/microorm-database/src/lib/micro-orm-database.module.ts new file mode 100644 index 00000000..a742dfdd --- /dev/null +++ b/libs/microorm-database/src/lib/micro-orm-database.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { MikroOrmModule } from '@mikro-orm/nestjs'; + +import { config } from './config'; + +@Module({ + imports: [MikroOrmModule.forRoot(config)], + exports: [MikroOrmModule], +}) +export class MicroOrmDatabaseModule {} diff --git a/libs/microorm-database/src/lib/migrations/.snapshot-microorm-test.json b/libs/microorm-database/src/lib/migrations/.snapshot-microorm-test.json new file mode 100644 index 00000000..52e0868e --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/.snapshot-microorm-test.json @@ -0,0 +1,726 @@ +{ + "namespaces": [ + "public" + ], + "name": "public", + "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "serial", + "unsigned": false, + "autoincrement": true, + "primary": true, + "nullable": false, + "mappedType": "integer" + }, + "city": { + "name": "city", + "type": "varchar", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 70, + "mappedType": "string" + }, + "state": { + "name": "state", + "type": "varchar", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 70, + "mappedType": "string" + }, + "country": { + "name": "country", + "type": "varchar", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 68, + "mappedType": "string" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + } + }, + "name": "addresses", + "schema": "public", + "indexes": [ + { + "keyName": "addresses_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {}, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "uuid_generate_v4()", + "mappedType": "uuid" + }, + "text": { + "name": "text", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + } + }, + "name": "book_list", + "schema": "public", + "indexes": [ + { + "keyName": "book_list_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {}, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "serial", + "unsigned": false, + "autoincrement": true, + "primary": true, + "nullable": false, + "mappedType": "integer" + }, + "name": { + "name": "name", + "type": "varchar(128)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 128, + "default": "'NULL'", + "mappedType": "string" + }, + "key": { + "name": "key", + "type": "varchar(128)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 128, + "mappedType": "string" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + } + }, + "name": "roles", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "key" + ], + "composite": false, + "keyName": "roles_key_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "keyName": "roles_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {}, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "serial", + "unsigned": false, + "autoincrement": true, + "primary": true, + "nullable": false, + "mappedType": "integer" + }, + "login": { + "name": "login", + "type": "varchar(100)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 100, + "mappedType": "string" + }, + "first_name": { + "name": "first_name", + "type": "varchar(100)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 100, + "default": "'NULL'", + "mappedType": "string" + }, + "last_name": { + "name": "last_name", + "type": "varchar(100)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 100, + "default": "'NULL'", + "mappedType": "string" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "default": "false", + "mappedType": "boolean" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "addresses_id": { + "name": "addresses_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "manager_id": { + "name": "manager_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "integer" + } + }, + "name": "users", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "login" + ], + "composite": false, + "keyName": "users_login_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "columnNames": [ + "addresses_id" + ], + "composite": false, + "keyName": "users_addresses_id_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "columnNames": [ + "manager_id" + ], + "composite": false, + "keyName": "users_manager_id_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "keyName": "users_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "users_addresses_id_foreign": { + "constraintName": "users_addresses_id_foreign", + "columnNames": [ + "addresses_id" + ], + "localTableName": "public.users", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.addresses", + "updateRule": "cascade" + }, + "users_manager_id_foreign": { + "constraintName": "users_manager_id_foreign", + "columnNames": [ + "manager_id" + ], + "localTableName": "public.users", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "deleteRule": "set null", + "updateRule": "cascade" + } + }, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "serial", + "unsigned": false, + "autoincrement": true, + "primary": true, + "nullable": false, + "mappedType": "integer" + }, + "text": { + "name": "text", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "kind": { + "name": "kind", + "type": "comment_kind_enum", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "nativeEnumName": "comment_kind_enum", + "enumItems": [ + "COMMENT", + "MESSAGE", + "NOTE" + ], + "mappedType": "enum" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(0) without time zone", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 0, + "default": "current_timestamp(0)", + "mappedType": "datetime" + }, + "created_by": { + "name": "created_by", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + } + }, + "name": "comments", + "schema": "public", + "indexes": [ + { + "keyName": "comments_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "comments_created_by_foreign": { + "constraintName": "comments_created_by_foreign", + "columnNames": [ + "created_by" + ], + "localTableName": "public.comments", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "updateRule": "cascade" + } + }, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "users_id": { + "name": "users_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "book_list_id": { + "name": "book_list_id", + "type": "uuid", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "uuid" + } + }, + "name": "users_have_book", + "schema": "public", + "indexes": [ + { + "keyName": "users_have_book_pkey", + "columnNames": [ + "users_id", + "book_list_id" + ], + "composite": true, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "users_have_book_users_id_foreign": { + "constraintName": "users_have_book_users_id_foreign", + "columnNames": [ + "users_id" + ], + "localTableName": "public.users_have_book", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "deleteRule": "cascade", + "updateRule": "cascade" + }, + "users_have_book_book_list_id_foreign": { + "constraintName": "users_have_book_book_list_id_foreign", + "columnNames": [ + "book_list_id" + ], + "localTableName": "public.users_have_book", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.book_list", + "deleteRule": "cascade", + "updateRule": "cascade" + } + }, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + }, + { + "columns": { + "users_id": { + "name": "users_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "roles_id": { + "name": "roles_id", + "type": "int", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + } + }, + "name": "users_have_roles", + "schema": "public", + "indexes": [ + { + "keyName": "users_have_roles_pkey", + "columnNames": [ + "users_id", + "roles_id" + ], + "composite": true, + "constraint": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "users_have_roles_users_id_foreign": { + "constraintName": "users_have_roles_users_id_foreign", + "columnNames": [ + "users_id" + ], + "localTableName": "public.users_have_roles", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.users", + "deleteRule": "cascade", + "updateRule": "cascade" + }, + "users_have_roles_roles_id_foreign": { + "constraintName": "users_have_roles_roles_id_foreign", + "columnNames": [ + "roles_id" + ], + "localTableName": "public.users_have_roles", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.roles", + "deleteRule": "cascade", + "updateRule": "cascade" + } + }, + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } + } + ], + "nativeEnums": { + "comment_kind_enum": { + "name": "comment_kind_enum", + "schema": "public", + "items": [ + "COMMENT", + "MESSAGE", + "NOTE" + ] + } + } +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123104848_CreateUsersTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123104848_CreateUsersTable.ts new file mode 100644 index 00000000..9b142690 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123104848_CreateUsersTable.ts @@ -0,0 +1,14 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123104848_CreateUsersTable extends Migration { + + override async up(): Promise { + this.addSql(`create table "users" ("id" serial primary key, "login" varchar(100) not null, "first_name" varchar(100) null default 'NULL', "last_name" varchar(100) null default 'NULL', "is_active" boolean null default false, "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0));`); + this.addSql(`alter table "users" add constraint "users_login_unique" unique ("login");`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "users" cascade;`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123105611_CreateAddressesTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123105611_CreateAddressesTable.ts new file mode 100644 index 00000000..087dc967 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123105611_CreateAddressesTable.ts @@ -0,0 +1,13 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123105611_CreateAddressesTable extends Migration { + + override async up(): Promise { + this.addSql(`create table "addresses" ("id" serial primary key, "city" varchar(70) null, "state" varchar(70) null, "country" varchar(68) null, "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0));`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "addresses" cascade;`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123110115_CreateRolesTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123110115_CreateRolesTable.ts new file mode 100644 index 00000000..e5fc0f89 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123110115_CreateRolesTable.ts @@ -0,0 +1,14 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123110115_CreateRolesTable extends Migration { + + override async up(): Promise { + this.addSql(`create table "roles" ("id" serial primary key, "name" varchar(128) null default 'NULL', "key" varchar(128) not null, "is_default" boolean not null default 'false', "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0));`); + this.addSql(`alter table "roles" add constraint "roles_key_unique" unique ("key");`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "roles" cascade;`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123111042_CreateCommentsTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123111042_CreateCommentsTable.ts new file mode 100644 index 00000000..84816d8c --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123111042_CreateCommentsTable.ts @@ -0,0 +1,16 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123111042_CreateCommentsTable extends Migration { + + override async up(): Promise { + this.addSql(`create type "comment_kind_enum" as enum ('COMMENT', 'MESSAGE', 'NOTE');`); + this.addSql(`create table "comments" ("id" serial primary key, "text" text not null, "kind" "comment_kind_enum" not null, "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0));`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "comments" cascade;`); + + this.addSql(`drop type "comment_kind_enum";`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts new file mode 100644 index 00000000..cf00a86f --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts @@ -0,0 +1,16 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123123708_CreateUsersRolesRelations extends Migration { + + override async up(): Promise { + this.addSql(`create table "users_have_roles" ("users_id" int not null, "roles_id" int not null, constraint "users_have_roles_pkey" primary key ("users_id", "roles_id"));`); + + this.addSql(`alter table "users_have_roles" add constraint "users_have_roles_users_id_foreign" foreign key ("users_id") references "users" ("id") on update cascade on delete cascade;`); + this.addSql(`alter table "users_have_roles" add constraint "users_have_roles_roles_id_foreign" foreign key ("roles_id") references "roles" ("id") on update cascade on delete cascade;`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "users_have_roles" cascade;`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123124745_CreateUsersUsersRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123124745_CreateUsersUsersRelations.ts new file mode 100644 index 00000000..58cdf1a6 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123124745_CreateUsersUsersRelations.ts @@ -0,0 +1,18 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123124745_CreateUsersUsersRelations extends Migration { + + override async up(): Promise { + this.addSql(`alter table "users" add column "manager_id" int null;`); + this.addSql(`alter table "users" add constraint "users_manager_id_foreign" foreign key ("manager_id") references "users" ("id") on update cascade on delete set null;`); + this.addSql(`alter table "users" add constraint "users_manager_id_unique" unique ("manager_id");`); + } + + override async down(): Promise { + this.addSql(`alter table "users" drop constraint "users_manager_id_foreign";`); + + this.addSql(`alter table "users" drop constraint "users_manager_id_unique";`); + this.addSql(`alter table "users" drop column "manager_id";`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123125941_CreateUsersAddressRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123125941_CreateUsersAddressRelations.ts new file mode 100644 index 00000000..c866f5b3 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123125941_CreateUsersAddressRelations.ts @@ -0,0 +1,18 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123125941_CreateUsersAddressRelations extends Migration { + + override async up(): Promise { + this.addSql(`alter table "users" add column "addresses_id" int not null;`); + this.addSql(`alter table "users" add constraint "users_addresses_id_foreign" foreign key ("addresses_id") references "addresses" ("id") on update cascade;`); + this.addSql(`alter table "users" add constraint "users_addresses_id_unique" unique ("addresses_id");`); + } + + override async down(): Promise { + this.addSql(`alter table "users" drop constraint "users_addresses_id_foreign";`); + + this.addSql(`alter table "users" drop constraint "users_addresses_id_unique";`); + this.addSql(`alter table "users" drop column "addresses_id";`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123130345_CreateUsersCommentsRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123130345_CreateUsersCommentsRelations.ts new file mode 100644 index 00000000..223764d3 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123130345_CreateUsersCommentsRelations.ts @@ -0,0 +1,16 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123130345_CreateUsersCommentsRelations extends Migration { + + override async up(): Promise { + this.addSql(`alter table "comments" add column "created_by" int not null;`); + this.addSql(`alter table "comments" add constraint "comments_created_by_foreign" foreign key ("created_by") references "users" ("id") on update cascade;`); + } + + override async down(): Promise { + this.addSql(`alter table "comments" drop constraint "comments_created_by_foreign";`); + + this.addSql(`alter table "comments" drop column "created_by";`); + } + +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123131039_CreateBookListTable.ts b/libs/microorm-database/src/lib/migrations/Migration20250123131039_CreateBookListTable.ts new file mode 100644 index 00000000..4392881d --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123131039_CreateBookListTable.ts @@ -0,0 +1,14 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123131039_CreateBookListTable extends Migration { + override async up(): Promise { + this.addSql(`create extension if not exists "uuid-ossp";`); + this.addSql( + `create table "book_list" ("id" uuid not null default uuid_generate_v4(), "text" text not null, "created_at" timestamp(0) without time zone null default current_timestamp(0), "updated_at" timestamp(0) without time zone null default current_timestamp(0), constraint "book_list_pkey" primary key ("id"));` + ); + } + + override async down(): Promise { + this.addSql(`drop table if exists "book_list" cascade;`); + } +} diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123131438_CreateUsersBookListRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123131438_CreateUsersBookListRelations.ts new file mode 100644 index 00000000..35df79b8 --- /dev/null +++ b/libs/microorm-database/src/lib/migrations/Migration20250123131438_CreateUsersBookListRelations.ts @@ -0,0 +1,16 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20250123131438_CreateUsersBookListRelations extends Migration { + + override async up(): Promise { + this.addSql(`create table "users_have_book" ("users_id" int not null, "book_list_id" uuid not null, constraint "users_have_book_pkey" primary key ("users_id", "book_list_id"));`); + + this.addSql(`alter table "users_have_book" add constraint "users_have_book_users_id_foreign" foreign key ("users_id") references "users" ("id") on update cascade on delete cascade;`); + this.addSql(`alter table "users_have_book" add constraint "users_have_book_book_list_id_foreign" foreign key ("book_list_id") references "book_list" ("id") on update cascade on delete cascade;`); + } + + override async down(): Promise { + this.addSql(`drop table if exists "users_have_book" cascade;`); + } + +} diff --git a/libs/microorm-database/tsconfig.json b/libs/microorm-database/tsconfig.json new file mode 100644 index 00000000..6f7169a3 --- /dev/null +++ b/libs/microorm-database/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/microorm-database/tsconfig.lib.json b/libs/microorm-database/tsconfig.lib.json new file mode 100644 index 00000000..c297a248 --- /dev/null +++ b/libs/microorm-database/tsconfig.lib.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"], + "target": "es2021", + "strictNullChecks": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/libs/microorm-database/tsconfig.spec.json b/libs/microorm-database/tsconfig.spec.json new file mode 100644 index 00000000..0d3c604e --- /dev/null +++ b/libs/microorm-database/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "moduleResolution": "node10", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/libs/typeorm-database/.eslintrc.json b/libs/typeorm-database/.eslintrc.json new file mode 100644 index 00000000..8d4e2111 --- /dev/null +++ b/libs/typeorm-database/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["../../.eslintrc.base.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + } + ] +} diff --git a/libs/typeorm-database/README.md b/libs/typeorm-database/README.md new file mode 100644 index 00000000..d231088b --- /dev/null +++ b/libs/typeorm-database/README.md @@ -0,0 +1,7 @@ +# typeorm-database + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test typeorm-database` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/database/jest.config.ts b/libs/typeorm-database/jest.config.ts similarity index 72% rename from libs/database/jest.config.ts rename to libs/typeorm-database/jest.config.ts index 6fef25b7..f4c0c3b5 100644 --- a/libs/database/jest.config.ts +++ b/libs/typeorm-database/jest.config.ts @@ -1,11 +1,11 @@ /* eslint-disable */ export default { - displayName: 'database', + displayName: 'typeorm-database', preset: '../../jest.preset.js', testEnvironment: 'node', transform: { '^.+\\.[tj]s$': ['ts-jest', { tsconfig: '/tsconfig.spec.json' }], }, moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/libs/database', + coverageDirectory: '../../coverage/libs/typeorm-database', }; diff --git a/libs/typeorm-database/project.json b/libs/typeorm-database/project.json new file mode 100644 index 00000000..58f32e84 --- /dev/null +++ b/libs/typeorm-database/project.json @@ -0,0 +1,9 @@ +{ + "name": "typeorm-database", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/typeorm-database/src", + "projectType": "library", + "tags": [], + "// targets": "to see all targets run: nx show project typeorm-database --web", + "targets": {} +} diff --git a/libs/typeorm-database/src/index.ts b/libs/typeorm-database/src/index.ts new file mode 100644 index 00000000..4f2a7e75 --- /dev/null +++ b/libs/typeorm-database/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/type-orm-database.module'; +export * from './lib/entities'; diff --git a/libs/database/src/lib/config-cli.ts b/libs/typeorm-database/src/lib/config-cli.ts similarity index 100% rename from libs/database/src/lib/config-cli.ts rename to libs/typeorm-database/src/lib/config-cli.ts diff --git a/libs/database/src/lib/config.ts b/libs/typeorm-database/src/lib/config.ts similarity index 100% rename from libs/database/src/lib/config.ts rename to libs/typeorm-database/src/lib/config.ts diff --git a/libs/database/src/lib/entities/addresses.ts b/libs/typeorm-database/src/lib/entities/addresses.ts similarity index 100% rename from libs/database/src/lib/entities/addresses.ts rename to libs/typeorm-database/src/lib/entities/addresses.ts diff --git a/libs/database/src/lib/entities/book-list.ts b/libs/typeorm-database/src/lib/entities/book-list.ts similarity index 100% rename from libs/database/src/lib/entities/book-list.ts rename to libs/typeorm-database/src/lib/entities/book-list.ts diff --git a/libs/database/src/lib/entities/comments.ts b/libs/typeorm-database/src/lib/entities/comments.ts similarity index 100% rename from libs/database/src/lib/entities/comments.ts rename to libs/typeorm-database/src/lib/entities/comments.ts diff --git a/libs/database/src/lib/entities/index.ts b/libs/typeorm-database/src/lib/entities/index.ts similarity index 100% rename from libs/database/src/lib/entities/index.ts rename to libs/typeorm-database/src/lib/entities/index.ts diff --git a/libs/database/src/lib/entities/roles.ts b/libs/typeorm-database/src/lib/entities/roles.ts similarity index 100% rename from libs/database/src/lib/entities/roles.ts rename to libs/typeorm-database/src/lib/entities/roles.ts diff --git a/libs/database/src/lib/entities/users-have-roles.ts b/libs/typeorm-database/src/lib/entities/users-have-roles.ts similarity index 100% rename from libs/database/src/lib/entities/users-have-roles.ts rename to libs/typeorm-database/src/lib/entities/users-have-roles.ts diff --git a/libs/database/src/lib/entities/users.ts b/libs/typeorm-database/src/lib/entities/users.ts similarity index 100% rename from libs/database/src/lib/entities/users.ts rename to libs/typeorm-database/src/lib/entities/users.ts diff --git a/libs/database/src/lib/migrations/1607701631900-CreateAddressesTable.ts b/libs/typeorm-database/src/lib/migrations/1607701631900-CreateAddressesTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1607701631900-CreateAddressesTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701631900-CreateAddressesTable.ts diff --git a/libs/database/src/lib/migrations/1607701632000-CreateUsersTable.ts b/libs/typeorm-database/src/lib/migrations/1607701632000-CreateUsersTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1607701632000-CreateUsersTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701632000-CreateUsersTable.ts diff --git a/libs/database/src/lib/migrations/1607701632200-CreateRolesTable.ts b/libs/typeorm-database/src/lib/migrations/1607701632200-CreateRolesTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1607701632200-CreateRolesTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701632200-CreateRolesTable.ts diff --git a/libs/database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts b/libs/typeorm-database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts diff --git a/libs/database/src/lib/migrations/1607701632600-CreateCommentsTable.ts b/libs/typeorm-database/src/lib/migrations/1607701632600-CreateCommentsTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1607701632600-CreateCommentsTable.ts rename to libs/typeorm-database/src/lib/migrations/1607701632600-CreateCommentsTable.ts diff --git a/libs/database/src/lib/migrations/1665469071344-CreateBookTable.ts b/libs/typeorm-database/src/lib/migrations/1665469071344-CreateBookTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1665469071344-CreateBookTable.ts rename to libs/typeorm-database/src/lib/migrations/1665469071344-CreateBookTable.ts diff --git a/libs/database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts b/libs/typeorm-database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts similarity index 100% rename from libs/database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts rename to libs/typeorm-database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts diff --git a/libs/database/src/lib/seeders/factory/addresses.factory.ts b/libs/typeorm-database/src/lib/seeders/factory/addresses.factory.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/addresses.factory.ts rename to libs/typeorm-database/src/lib/seeders/factory/addresses.factory.ts diff --git a/libs/database/src/lib/seeders/factory/comments.factory.ts b/libs/typeorm-database/src/lib/seeders/factory/comments.factory.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/comments.factory.ts rename to libs/typeorm-database/src/lib/seeders/factory/comments.factory.ts diff --git a/libs/database/src/lib/seeders/factory/index.ts b/libs/typeorm-database/src/lib/seeders/factory/index.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/index.ts rename to libs/typeorm-database/src/lib/seeders/factory/index.ts diff --git a/libs/database/src/lib/seeders/factory/roles.factory.ts b/libs/typeorm-database/src/lib/seeders/factory/roles.factory.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/roles.factory.ts rename to libs/typeorm-database/src/lib/seeders/factory/roles.factory.ts diff --git a/libs/database/src/lib/seeders/factory/user.factory.ts b/libs/typeorm-database/src/lib/seeders/factory/user.factory.ts similarity index 100% rename from libs/database/src/lib/seeders/factory/user.factory.ts rename to libs/typeorm-database/src/lib/seeders/factory/user.factory.ts diff --git a/libs/database/src/lib/seeders/root.seeder.ts b/libs/typeorm-database/src/lib/seeders/root.seeder.ts similarity index 100% rename from libs/database/src/lib/seeders/root.seeder.ts rename to libs/typeorm-database/src/lib/seeders/root.seeder.ts diff --git a/libs/database/src/lib/database.module.ts b/libs/typeorm-database/src/lib/type-orm-database.module.ts similarity index 84% rename from libs/database/src/lib/database.module.ts rename to libs/typeorm-database/src/lib/type-orm-database.module.ts index 7a796bf4..39a65f32 100644 --- a/libs/database/src/lib/database.module.ts +++ b/libs/typeorm-database/src/lib/type-orm-database.module.ts @@ -7,4 +7,4 @@ import { config } from './config'; imports: [TypeOrmModule.forRoot(config)], exports: [TypeOrmModule], }) -export class DatabaseModule {} +export class TypeOrmDatabaseModule {} diff --git a/libs/database/tsconfig.json b/libs/typeorm-database/tsconfig.json similarity index 100% rename from libs/database/tsconfig.json rename to libs/typeorm-database/tsconfig.json diff --git a/libs/database/tsconfig.lib.json b/libs/typeorm-database/tsconfig.lib.json similarity index 100% rename from libs/database/tsconfig.lib.json rename to libs/typeorm-database/tsconfig.lib.json diff --git a/libs/database/tsconfig.spec.json b/libs/typeorm-database/tsconfig.spec.json similarity index 100% rename from libs/database/tsconfig.spec.json rename to libs/typeorm-database/tsconfig.spec.json diff --git a/package-lock.json b/package-lock.json index bbf4ea72..09a70518 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,9 @@ "@angular/platform-browser": "19.0.3", "@angular/platform-browser-dynamic": "19.0.3", "@angular/router": "19.0.3", + "@mikro-orm/cli": "^6.4.3", "@mikro-orm/core": "^6.4.3", + "@mikro-orm/migrations": "^6.4.3", "@mikro-orm/mysql": "^6.4.3", "@mikro-orm/nestjs": "^6.0.2", "@mikro-orm/postgresql": "^6.4.3", @@ -3743,6 +3745,31 @@ "node": ">=8" } }, + "node_modules/@jercle/yargonaut": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@jercle/yargonaut/-/yargonaut-1.1.5.tgz", + "integrity": "sha512-zBp2myVvBHp1UaJsNTyS6q4UDKT7eRiqTS4oNTS6VQMd6mpxYOdbeK4pY279cDCdakGy6hG0J3ejoXZVsPwHqw==", + "dependencies": { + "chalk": "^4.1.2", + "figlet": "^1.5.2", + "parent-require": "^1.0.0" + } + }, + "node_modules/@jercle/yargonaut/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@jest/console": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", @@ -4457,6 +4484,39 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==" }, + "node_modules/@mikro-orm/cli": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.3.tgz", + "integrity": "sha512-DWnYNxoyMgU6L90TGBlT0eziTu6yl15ArnnFoq0kyOjp8JEMRjin+8cizSrKyQ3QiQZ5iop5fB0i9Sp+Hbgd8Q==", + "dependencies": { + "@jercle/yargonaut": "1.1.5", + "@mikro-orm/core": "6.4.3", + "@mikro-orm/knex": "6.4.3", + "fs-extra": "11.2.0", + "tsconfig-paths": "4.2.0", + "yargs": "17.7.2" + }, + "bin": { + "mikro-orm": "cli", + "mikro-orm-esm": "esm" + }, + "engines": { + "node": ">= 18.12.0" + } + }, + "node_modules/@mikro-orm/cli/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/@mikro-orm/core": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.3.tgz", @@ -4538,6 +4598,35 @@ "node": ">=14.14" } }, + "node_modules/@mikro-orm/migrations": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mikro-orm/migrations/-/migrations-6.4.3.tgz", + "integrity": "sha512-VrsKq95esUBEMhwp9vVX+YUj2+/cNwb8UZ63HfgaqPo+pYj8r1RBSTboFOE9V0Md0n3ol9b5xByfPPa3qHmL0g==", + "dependencies": { + "@mikro-orm/knex": "6.4.3", + "fs-extra": "11.2.0", + "umzug": "3.8.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0" + } + }, + "node_modules/@mikro-orm/migrations/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/@mikro-orm/mysql": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/@mikro-orm/mysql/-/mysql-6.4.3.tgz", @@ -8619,6 +8708,174 @@ "node": ">=16.0.0" } }, + "node_modules/@rushstack/node-core-library": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-5.10.2.tgz", + "integrity": "sha512-xOF/2gVJZTfjTxbo4BDj9RtQq/HFnrrKdtem4JkyRLnwsRz2UDTg8gA1/et10fBx5RxmZD9bYVGST69W8ME5OQ==", + "dependencies": { + "ajv": "~8.13.0", + "ajv-draft-04": "~1.0.0", + "ajv-formats": "~3.0.1", + "fs-extra": "~7.0.1", + "import-lazy": "~4.0.0", + "jju": "~1.4.0", + "resolve": "~1.22.1", + "semver": "~7.5.4" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.4.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@rushstack/node-core-library/node_modules/fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/@rushstack/node-core-library/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/@rushstack/terminal": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@rushstack/terminal/-/terminal-0.14.5.tgz", + "integrity": "sha512-TEOpNwwmsZVrkp0omnuTUTGZRJKTr6n6m4OITiNjkqzLAkcazVpwR1SOtBg6uzpkIBLgrcNHETqI8rbw3uiUfw==", + "dependencies": { + "@rushstack/node-core-library": "5.10.2", + "supports-color": "~8.1.1" + }, + "peerDependencies": { + "@types/node": "*" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@rushstack/terminal/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/@rushstack/ts-command-line": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.3.tgz", + "integrity": "sha512-HazKL8fv4HMQMzrKJCrOrhyBPPdzk7iajUXgsASwjQ8ROo1cmgyqxt/k9+SdmrNLGE1zATgRqMUH3s/6smbRMA==", + "dependencies": { + "@rushstack/terminal": "0.14.5", + "@types/argparse": "1.0.38", + "argparse": "~1.0.9", + "string-argv": "~0.3.1" + } + }, + "node_modules/@rushstack/ts-command-line/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@rushstack/ts-command-line/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, "node_modules/@schematics/angular": { "version": "19.0.4", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.0.4.tgz", @@ -9129,6 +9386,11 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/argparse": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/@types/argparse/-/argparse-1.0.38.tgz", + "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -10727,7 +10989,7 @@ "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, + "devOptional": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -10739,6 +11001,19 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-draft-04": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", + "integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==", + "peerDependencies": { + "ajv": "^8.5.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/ajv-formats": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", @@ -13552,7 +13827,6 @@ "version": "0.13.1", "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, "engines": { "node": ">=12" }, @@ -14522,8 +14796,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -14609,6 +14882,17 @@ "bser": "2.1.1" } }, + "node_modules/figlet": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.8.0.tgz", + "integrity": "sha512-chzvGjd+Sp7KUvPHZv6EXV5Ir3Q7kYNpCr4aHrRW79qFtTefmQZNny+W1pW9kf5zeE6dikku2W50W/wAH2xWgw==", + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -16046,6 +16330,14 @@ "node": ">=4" } }, + "node_modules/import-lazy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", + "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", + "engines": { + "node": ">=8" + } + }, "node_modules/import-local": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", @@ -17550,6 +17842,11 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17691,8 +17988,7 @@ "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify": { "version": "1.1.1", @@ -17728,7 +18024,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, @@ -20868,6 +21163,14 @@ "node": ">=6" } }, + "node_modules/parent-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz", + "integrity": "sha512-2MXDNZC4aXdkkap+rBBMv0lUsfJqvX5/2FiYYnfCnorZt3Pk06/IOR5KeaoghgS2w07MLWgjbsnyaq6PdHn2LQ==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -21402,6 +21705,14 @@ "node": ">=4" } }, + "node_modules/pony-cause": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/pony-cause/-/pony-cause-2.1.11.tgz", + "integrity": "sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/portfinder": { "version": "1.0.32", "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", @@ -22263,7 +22574,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "engines": { "node": ">=6" } @@ -22582,7 +22892,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -23794,6 +24103,14 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -24706,7 +25023,6 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", @@ -24750,7 +25066,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, "engines": { "node": ">=4" } @@ -25118,6 +25433,32 @@ "node": ">=8" } }, + "node_modules/umzug": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-3.8.2.tgz", + "integrity": "sha512-BEWEF8OJjTYVC56GjELeHl/1XjFejrD7aHzn+HldRJTx+pL1siBrKHZC8n4K/xL3bEzVA9o++qD1tK2CpZu4KA==", + "dependencies": { + "@rushstack/ts-command-line": "^4.12.2", + "emittery": "^0.13.0", + "fast-glob": "^3.3.2", + "pony-cause": "^2.1.4", + "type-fest": "^4.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/umzug/node_modules/type-fest": { + "version": "4.33.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.33.0.tgz", + "integrity": "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -25272,7 +25613,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } diff --git a/package.json b/package.json index 3f68ef59..80a0c276 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,12 @@ "version": "0.0.0", "license": "MIT", "scripts": { - "typeorm": "ts-node -r tsconfig-paths/register -r dotenv/config --project libs/database/tsconfig.lib.json ./node_modules/typeorm/cli.js -d libs/database/src/lib/config-cli.ts", + "typeorm": "ts-node -r tsconfig-paths/register -r dotenv/config --project libs/typeorm-database/tsconfig.lib.json ./node_modules/typeorm/cli.js -d libs/typeorm-database/src/lib/config-cli.ts", "typeorm:run": "npm run typeorm migration:run", "typeorm:revert": "npm run typeorm migration:revert", - "seed:run": "ts-node -r tsconfig-paths/register -r dotenv/config --project libs/database/tsconfig.lib.json ./node_modules/@jorgebodega/typeorm-seeding/dist/cli.js -d libs/database/src/lib/config-cli.ts seed libs/database/src/lib/seeders/root.seeder.ts", + "seed:run": "ts-node -r tsconfig-paths/register -r dotenv/config --project libs/typeorm-database/tsconfig.lib.json ./node_modules/@jorgebodega/typeorm-seeding/dist/cli.js -d libs/typeorm-database/src/lib/config-cli.ts seed libs/typeorm-database/src/lib/seeders/root.seeder.ts", + "microorm": "ts-node -r tsconfig-paths/register -r dotenv/config -P ./libs/microorm-database/tsconfig.lib.json ./node_modules/@mikro-orm/cli/cli.js", + "microorm:up": "npm run microorm migration:up", "demo:json-api": "nx run json-api-server:serve:development", "demo:json-api-front": "nx run json-api-front:serve:development" }, @@ -21,7 +23,9 @@ "@angular/platform-browser": "19.0.3", "@angular/platform-browser-dynamic": "19.0.3", "@angular/router": "19.0.3", + "@mikro-orm/cli": "^6.4.3", "@mikro-orm/core": "^6.4.3", + "@mikro-orm/migrations": "^6.4.3", "@mikro-orm/mysql": "^6.4.3", "@mikro-orm/nestjs": "^6.0.2", "@mikro-orm/postgresql": "^6.4.3", @@ -106,5 +110,11 @@ }, "nx": { "includedScripts": [] + }, + "mikro-orm": { + "configPaths": [ + "libs/microorm-database/src/lib/config-cli.ts" + ], + "tsConfigPath": "./libs/microorm-database/tsconfig.lib.json" } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 06afef18..b4cd5ce3 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -36,8 +36,13 @@ "@klerick/nestjs-json-rpc-sdk/ngModule": [ "libs/json-rpc/nestjs-json-rpc-sdk/src/ngModule.ts" ], + "@nestjs-json-api/microorm-database": [ + "libs/microorm-database/src/index.ts" + ], "@nestjs-json-api/type-for-rpc": ["libs/type-for-rpc/src/index.ts"], - "database": ["libs/database/src/index.ts"] + "@nestjs-json-api/typeorm-database": [ + "libs/typeorm-database/src/index.ts" + ] } }, "exclude": ["node_modules", "tmp"] From 8cd79557f1f9d27ba7a6082cee57d5bf959eb9f5 Mon Sep 17 00:00:00 2001 From: Alex H Date: Sat, 25 Jan 2025 07:36:04 +0100 Subject: [PATCH 14/26] refactor(json-api-nestjs): add method for validate from microorm --- .../src/lib/mock-utils/typeorm/index.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/index.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/index.ts new file mode 100644 index 00000000..f7eba83d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/index.ts @@ -0,0 +1,60 @@ +import { TypeOrmModule } from '@nestjs/typeorm'; +import { DynamicModule } from '@nestjs/common'; +import { IMemoryDb } from 'pg-mem'; + +import { + Addresses, + Comments, + Notes, + Pods, + Requests, + RequestsHavePodLocks, + Roles, + UserGroups, + Users, +} from './entities'; +import { DataSource } from 'typeorm'; + +export * from './entities'; +export * from './utils'; + +export const entities = [ + Users, + UserGroups, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + Notes, +]; + +export function mockDBTestModule(db: IMemoryDb): DynamicModule { + return TypeOrmModule.forRootAsync({ + useFactory() { + return { + type: 'postgres', + // logging: true, + entities: [ + Users, + UserGroups, + Roles, + RequestsHavePodLocks, + Requests, + Pods, + Comments, + Addresses, + Notes, + ], + }; + }, + async dataSourceFactory(options) { + const dataSource: DataSource = await db.adapters.createTypeormDataSource( + options + ); + + return dataSource; + }, + }); +} From 18f4a0cad13bff1869e2c415aed0a29eefa78024 Mon Sep 17 00:00:00 2001 From: Alex H Date: Mon, 10 Feb 2025 10:45:37 +0100 Subject: [PATCH 15/26] feat(json-api-nestjs): Microro orm Create orm methode, create general transform service, use this service for swagger and zod --- .../lib/mock-utils/microrom/utils/init-db.ts | 80 ++ .../orm-methods/delete-one/delete-one.spec.ts | 61 ++ .../orm-methods/delete-one/delete-one.ts | 20 + .../delete-relationship.spec.ts | 182 ++++ .../delete-relationship.ts | 44 + .../orm-methods/get-all/get-all.spec.ts | 975 ++++++++++++++++++ .../micro-orm/orm-methods/get-all/get-all.ts | 90 ++ .../get-all/get-query-for-count.spec.ts | 242 +++++ .../get-all/get-query-for-count.ts | 60 ++ .../orm-methods/get-one/get-one.spec.ts | 102 ++ .../micro-orm/orm-methods/get-one/get-one.ts | 29 + .../get-relationship/get-relationship.spec.ts | 144 +++ .../get-relationship/get-relationship.ts | 34 + .../modules/micro-orm/orm-methods/index.ts | 9 + .../orm-methods/patch-one/patch-one.spec.ts | 234 +++++ .../orm-methods/patch-one/patch-one.ts | 52 + .../patch-relationship.spec.ts | 134 +++ .../patch-relationship/patch-relationship.ts | 52 + .../orm-methods/post-one/post-one.spec.ts | 338 ++++++ .../orm-methods/post-one/post-one.ts | 23 + .../post-relationship.spec.ts | 147 +++ .../post-relationship/post-relationship.ts | 48 + .../service/micro-orm-util.service.spec.ts | 571 ++++++++++ .../service/micro-orm-util.service.ts | 726 +++++++++++++ .../json-api-transformer.service.spec.ts | 674 ++++++++++++ .../service/json-api-transformer.service.ts | 322 ++++++ 26 files changed, 5393 insertions(+) create mode 100644 libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts new file mode 100644 index 00000000..e1e8a83d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts @@ -0,0 +1,80 @@ +import { Knex as TypeKnex } from '@mikro-orm/knex'; +import { MikroORM } from '@mikro-orm/core'; +import { PostgreSqlDriver } from '@mikro-orm/postgresql'; +import { SqlHighlighter } from '@mikro-orm/sql-highlighter'; + +import Knex from 'knex'; +import * as ClientPgLite from 'knex-pglite'; + +import { + Addresses, + Comments, + Notes, + Roles, + UserGroups, + Users, +} from '../entities'; + +let knexInst: TypeKnex; + +export async function sharedConnect(): Promise { + // @ts-ignore + // return globalThis.pgLite; + + if (knexInst) { + return knexInst; + } + + const pgLite = await Promise.all([ + import('@electric-sql/pglite'), + // @ts-ignore + import('@electric-sql/pglite/contrib/uuid_ossp'), + ]).then( + ([{ PGlite }, { uuid_ossp }]) => + new PGlite({ + extensions: { uuid_ossp }, + }) + ); + + knexInst = Knex({ + // @ts-ignore + client: ClientPgLite, + dialect: 'postgres', + // @ts-ignore + connection: { pglite: pgLite }, + }); + + return knexInst; +} + +export async function initMikroOrm(knex: TypeKnex, testDbName: string) { + const result = await knex.raw( + `select 1 from pg_database where datname = '${testDbName}'` + ); + + if ((result['rows'] as []).length === 0) { + await knex.raw(`create database ??`, [testDbName]); + } + + const orm = await MikroORM.init({ + highlighter: new SqlHighlighter(), + driver: PostgreSqlDriver, + dbName: testDbName, + driverOptions: knexInst, + entities: [Users, UserGroups, Roles, Comments, Addresses, Notes], + allowGlobalContext: true, + schema: 'public', + debug: ['query', 'query-params'], + }); + + if ((result['rows'] as []).length === 0) { + const sql = await orm.getSchemaGenerator().getCreateSchemaSQL(); + const statements = sql.split(';').filter((s) => s.trim().length > 0); // Разбиваем на отдельные команды + + for (const statement of statements) { + await orm.em.execute(statement); + } + } + + return orm; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.spec.ts new file mode 100644 index 00000000..7563183a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.spec.ts @@ -0,0 +1,61 @@ +import { EntityManager, MikroORM } from '@mikro-orm/core'; + +import { + dbRandomName, + getModuleForPgLite, + pullData, + Users, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; +import { deleteOne } from './delete-one'; + +describe('delete-one', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('Delete one item', async () => { + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .limit(1) + .execute('get', true); + + await deleteOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, checkData.id); + + const result = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: checkData.id, + }) + .execute('get', true); + + expect(result).toBe(null); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.ts new file mode 100644 index 00000000..ccc07ab2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-one/delete-one.ts @@ -0,0 +1,20 @@ +import { MicroOrmService } from '../../service'; +import { ObjectLiteral } from '../../../../types'; + +export async function deleteOne( + this: MicroOrmService, + id: number | string +): Promise { + const data = await this.microOrmUtilService + .queryBuilder() + .where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }) + .getSingleResult(); + + if (!data) return void 0; + + await this.microOrmUtilService.entityManager.removeAndFlush(data); + + return void 0; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts new file mode 100644 index 00000000..ceebf884 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts @@ -0,0 +1,182 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { deleteRelationship } from './delete-relationship'; +import { BadRequestException } from '@nestjs/common'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +describe('delete-relationship', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups; + let notes: Collection; + let roles: Collection; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + const data = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, ['id']) + .leftJoinAndSelect('Users.comments', 'Comments_comments', {}, ['id']) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.notes', 'Notes__notes', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + roles: { + $exists: true, + }, + userGroup: { + $exists: true, + }, + }) + .limit(1) + .getSingleResult(); + + if (!data) throw new Error(); + ({ roles, notes, userGroup, addresses, comments, ...userObject as any } = + data); + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be ok', async () => { + const saveCount = roles.length; + const [roles1, roles2] = roles; + await deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', [ + { type: 'roles', id: roles1.id.toString() }, + ]); + + await deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', { + type: 'user-groups', + id: userGroup.id.toString(), + }); + + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + id: userObject.id, + }) + .getSingleResult(); + + expect(checkData?.roles.length).toBe(saveCount - 1); + expect(checkData?.roles.map((i) => i.id)).not.toContain(roles1.id); + expect(checkData?.userGroup).toBe(null); + }); + + it('should be error', async () => { + await expect( + deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', { + type: 'roles', + id: '1000', + }) + ).rejects.toThrow(); + + await expect( + deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', [ + { + type: 'roles', + id: '1000', + }, + ]) + ).rejects.toThrow(); + + await expect( + deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', [ + { + type: 'user-groups', + id: '10000', + }, + ]) + ).rejects.toThrow(); + await expect( + deleteRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', { + type: 'user-groups', + id: '10000', + }) + ).rejects.toThrow(); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts new file mode 100644 index 00000000..ca1f34a1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts @@ -0,0 +1,44 @@ +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; + +import { PostRelationshipData } from '../../../mixin/zod'; +import { MicroOrmService } from '../../service'; + +export async function deleteRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: MicroOrmService, + id: number | string, + rel: Rel, + input: PostRelationshipData +): Promise { + const idsResult = await this.microOrmUtilService.validateRelationInputData( + rel, + input + ); + + const currentEntityRef = this.microOrmUtilService.entityManager.getReference( + this.microOrmUtilService.entity, + id as any + ); + + if (Array.isArray(idsResult)) { + const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); + const relRef = idsResult.map((i) => + this.microOrmUtilService.entityManager.getReference(relEntity, i as any) + ); + currentEntityRef[rel].remove(...relRef); + } else { + if ( + currentEntityRef[rel][this.microOrmUtilService.getPrimaryNameFor(rel)] == + idsResult + ) { + // @ts-ignore + currentEntityRef[rel] = null; + } + } + + await this.microOrmUtilService.entityManager.flush(); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts new file mode 100644 index 00000000..6ad530ed --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts @@ -0,0 +1,975 @@ +import { EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getDefaultQuery, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { getAll } from './get-all'; + +describe('get-all', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + describe('page', () => { + it('default', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + + const checkData = await queryBuilder + .clone() + .limit(1) + .execute('all', true); + + const count = await queryBuilder.clone().count(); + const query = getDefaultQuery(); + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('not default page', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + + const checkData = await queryBuilder + .clone() + .limit(5, 5) + .execute('all', true); + const count = await queryBuilder.clone().count(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 2, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + + describe('order', () => { + it('sort target', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + const checkData = await queryBuilder + .clone() + .orderBy({ + lastName: 'DESC', + }) + .limit(5, 5) + .execute('all', true); + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.sort = { + target: { + lastName: 'DESC', + }, + }; + query.page = { + size: 5, + number: 2, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('sort relation 1:1', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder() + .select(['id', 'login', 'firstName', 'lastName']) + .leftJoinAndSelect('Users.addresses', 'Addresses__addresses', {}, [ + 'state', + ]); + + const checkData = await queryBuilder + .clone() + .orderBy({ + addresses: { + state: 'ASC', + id: 'ASC', + }, + }) + .limit(5, 5) + .execute('all', true); + + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.fields = { + target: ['login', 'firstName', 'lastName'], + addresses: ['state'], + }; + + query.sort = { + addresses: { + state: 'ASC', + id: 'ASC', + }, + }; + query.page = { + size: 5, + number: 2, + }; + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('sort relation m:m', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + const checkData = await queryBuilder + .clone() + .orderBy({ + roles: { + name: 'DESC', + }, + id: 'ASC', + }) + .limit(5, 5) + .execute('all', true); + + const count = await queryBuilder + .clone() + .leftJoin('Users.roles', 'Roles__roles') + .distinct() + .count(); + + const query = getDefaultQuery(); + query.sort = { + roles: { + name: 'DESC', + }, + }; + query.page = { + size: 5, + number: 2, + }; + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + + describe('select', () => { + it('default', async () => { + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + const checkData = await queryBuilder + .clone() + .limit(5) + .execute('all', true); + const count = await queryBuilder.clone().count(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('target select field', async () => { + const select: ['login', 'firstName', 'lastName'] = [ + 'login', + 'firstName', + 'lastName', + ]; + const queryBuilder = + microOrmServiceUser.microOrmUtilService.queryBuilder(); + const checkData = await queryBuilder + .clone() + .select(['id', ...select]) + .limit(5) + .execute('all', true); + const count = await queryBuilder.clone().count(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.fields = { + target: select, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('relation select field', async () => { + const select: ['login', 'firstName', 'lastName'] = [ + 'login', + 'firstName', + 'lastName', + ]; + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .select(['id', ...select]) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, [ + 'city', + 'state', + ]) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['name', 'key']); + const checkData = await queryBuilder + .clone() + .limit(5) + .execute('all', true); + const count = await queryBuilder.clone().count().distinct(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.fields = { + target: select, + addresses: ['city', 'state'], + roles: ['name', 'key'], + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + + describe('include', () => { + it('default', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .leftJoinAndSelect('Users.roles', 'Roles__roles'); + + const checkData = await queryBuilder + .clone() + .limit(5) + .execute('all', true); + const count = await queryBuilder.clone().count().distinct(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.include = ['roles']; + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('include with select', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.addresses', 'Addresses__addresses', {}, [ + 'city', + 'state', + ]); + + const checkData = await queryBuilder + .clone() + .limit(5) + .execute('all', true); + const count = await queryBuilder.clone().count().distinct(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.include = ['roles']; + query.fields = { + addresses: ['city', 'state'], + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + + describe('filter', () => { + let rolesData: Roles[]; + let addresses: Addresses[]; + let userGroups: UserGroups[]; + let users: Users[]; + let notes: Notes[]; + beforeAll(async () => { + rolesData = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Roles) + .execute('all', true); + addresses = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Addresses) + .execute('all', true); + + userGroups = await microOrmServiceUser.microOrmUtilService + .queryBuilder(UserGroups) + .execute('all', true); + users = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .execute('all', true); + notes = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Notes) + .execute('all', true); + }); + + describe('target', () => { + it('simple filter on target', async () => { + const randUsers = faker.helpers.arrayElements(users); + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + login: { + $in: randUsers.map((i) => i.login), + }, + }); + + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + login: 'DESC', + }) + .execute('all', true); + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + target: { + login: { + in: randUsers.map((i) => i.login) as [string, ...string[]], + }, + }, + }; + query.sort = { + target: { + login: 'DESC', + }, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('Target relation is null 1:1', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + manager: { + $exists: false, + }, + }); + + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + manager: { + login: 'DESC', + }, + }) + .execute('all', true); + const count = await queryBuilder.clone().count(); + + const queryBuilder2 = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + addresses: { + $exists: true, + }, + }); + + const checkData2 = await queryBuilder2 + .clone() + .limit(5) + .orderBy({ + addresses: { + city: 'DESC', + }, + }) + .execute('all', true); + const count2 = await queryBuilder2.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + target: { + manager: { + eq: 'null', + }, + }, + }; + query.sort = { + manager: { + login: 'DESC', + }, + }; + + const query2 = getDefaultQuery(); + query2.page = { + size: 5, + number: 1, + }; + query2.filter = { + target: { + addresses: { + ne: 'null', + }, + }, + }; + query2.sort = { + addresses: { + city: 'DESC', + }, + }; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + + const { totalItems: totalItems2, items: items2 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query2); + + expect(totalItems2).toBe(count2); + expect(JSON.stringify(items2)).toBe(JSON.stringify(checkData2)); + }); + + it('Target relation is null m:1 & 1:m', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + userGroup: { + $exists: false, + }, + }); + + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + login: 'DESC', + }) + .execute('all', true); + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + target: { + userGroup: { + eq: 'null', + }, + }, + }; + query.sort = { + target: { + login: 'DESC', + }, + }; + + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + + const checkData2 = await queryBuilder + .clone() + .limit(5) + .orderBy({ + userGroup: { + label: 'DESC', + }, + }) + .execute('all', true); + + const count2 = await queryBuilder.clone().count(); + + const query2 = getDefaultQuery(); + query2.page = { + size: 5, + number: 1, + }; + query2.filter = { + target: { + userGroup: { + eq: 'null', + }, + }, + }; + query2.sort = { + userGroup: { + label: 'DESC', + }, + }; + + const { totalItems: totalItems2, items: items2 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query2); + + expect(totalItems2).toBe(count2); + expect(JSON.stringify(items2)).toBe(JSON.stringify(checkData2)); + + const queryBuilder2 = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + notes: { + $exists: true, + }, + }); + const checkData3 = await queryBuilder2 + .clone() + .limit(5) + .orderBy({ + notes: { + id: 'DESC', + }, + }) + .execute('all', true); + + const count3 = await queryBuilder2.clone().count().distinct(); + + const query3 = getDefaultQuery(); + query3.page = { + size: 5, + number: 1, + }; + query3.filter = { + target: { + notes: { + ne: 'null', + }, + }, + }; + query3.sort = { + notes: { + id: 'DESC', + }, + }; + + const { totalItems: totalItems3, items: items3 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query3); + expect(totalItems3).toBe(count3); + expect(JSON.stringify(items3)).toBe(JSON.stringify(checkData3)); + + const checkData4 = await queryBuilder2 + .clone() + .limit(5) + .orderBy({ id: 'DESC' }) + .execute('all', true); + const count4 = await queryBuilder2.clone().count().distinct(); + + const query4 = getDefaultQuery(); + query4.page = { + size: 5, + number: 1, + }; + query4.filter = { + target: { + notes: { + ne: 'null', + }, + }, + }; + query4.sort = { + target: { + id: 'DESC', + }, + }; + + const { totalItems: totalItems4, items: items4 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query4); + expect(totalItems4).toBe(count4); + expect(JSON.stringify(items4)).toBe(JSON.stringify(checkData4)); + }); + + it('Target relation is null m:m', async () => { + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + roles: { + $exists: false, + }, + }); + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + manager: { + login: 'DESC', + }, + }) + .execute('all', true); + + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + target: { + roles: { + eq: 'null', + }, + }, + }; + query.sort = { + manager: { + login: 'DESC', + }, + }; + const { totalItems: totalItems, items: items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + + const queryBuilder1 = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + roles: { + $exists: true, + }, + }); + + const checkData1 = await queryBuilder1 + .clone() + .limit(5) + .orderBy({ + roles: { + key: 'DESC', + }, + }) + .execute('all', true); + + const count1 = await queryBuilder1.clone().count().distinct(); + + const query1 = getDefaultQuery(); + query1.page = { + size: 5, + number: 1, + }; + query1.filter = { + target: { + roles: { + ne: 'null', + }, + }, + }; + query1.sort = { + roles: { + key: 'DESC', + }, + }; + const { totalItems: totalItems1, items: items1 } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query1); + + expect(totalItems1).toBe(count1); + expect(JSON.stringify(items1)).toBe(JSON.stringify(checkData1)); + }); + }); + + describe('relation', () => { + it('relation 1:1', async () => { + const randAddresses = faker.helpers.arrayElements(addresses); + + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .where({ + addresses: { + id: { + $in: randAddresses.map((i) => i.id), + }, + }, + }) + .joinAndSelect('Users.addresses', 'Addresses'); + + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + addresses: { + city: 'DESC', + }, + }) + .execute('all', true); + + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + relation: { + addresses: { + id: { + in: randAddresses.map((i) => `${i.id}`) as [ + string, + ...string[] + ], + }, + }, + }, + }; + query.sort = { + addresses: { + city: 'DESC', + }, + }; + query.include = ['addresses']; + const { totalItems: totalItems, items: items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('relation 1:m', async () => { + const randNotes = faker.helpers.arrayElements(notes, 3); + + const quweryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .leftJoinAndSelect('Users.notes', 'Notes', { + id: { $in: randNotes.map((i) => i.id) }, + }) + .where({ + notes: { + id: { + $in: randNotes.map((i) => i.id), + }, + }, + }); + const checkData = await quweryBuilder + .clone() + .limit(5) + .orderBy({ + addresses: { + city: 'DESC', + }, + }) + .execute('all', true); + const count = await quweryBuilder.clone().count(); + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + relation: { + notes: { + id: { + in: randNotes.map((i) => `${i.id}`) as [string, ...string[]], + }, + }, + }, + }; + query.sort = { + addresses: { + city: 'DESC', + }, + }; + query.include = ['notes']; + const { totalItems: totalItems, items: items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + + it('relation m:m', async () => { + const randRoles = faker.helpers.arrayElements(rolesData, 3); + + const queryBuilder = microOrmServiceUser.microOrmUtilService + .queryBuilder('Users') + .leftJoinAndSelect('Users.roles', 'Roles__roles', { + key: randRoles[0].key, + }) + .where({ + roles: { + key: { + $eq: randRoles[0].key, + }, + }, + }); + const checkData = await queryBuilder + .clone() + .limit(5) + .orderBy({ + addresses: { + city: 'DESC', + }, + }) + .execute('all', true); + + const count = await queryBuilder.clone().count(); + + const query = getDefaultQuery(); + query.page = { + size: 5, + number: 1, + }; + query.filter = { + relation: { + roles: { + key: { + eq: `${randRoles[0].key}`, + }, + }, + }, + }; + query.sort = { + addresses: { + city: 'DESC', + }, + }; + + query.include = ['roles']; + + const { totalItems: totalItems, items: items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, query); + + expect(totalItems).toBe(count); + expect(JSON.stringify(items)).toBe(JSON.stringify(checkData)); + }); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts new file mode 100644 index 00000000..7a06f141 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts @@ -0,0 +1,90 @@ +import { QueryFlag } from '@mikro-orm/core'; + +import { ObjectLiteral } from '../../../../types'; +import { MicroOrmService } from '../../service'; +import { Query } from '../../../mixin/zod'; +import { getQueryForCount, getSortObject } from './get-query-for-count'; + +export async function getAll( + this: MicroOrmService, + query: Query +): Promise<{ + totalItems: number; + items: E[]; +}> { + const { page } = query; + const countSubQuery = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, ...[query]); + + const skip = (page.number - 1) * page.size; + const paginationQuery = countSubQuery + .clone() + .select(this.microOrmUtilService.currentPrimaryColumn) + .limit(page.size, skip); + + const collectIdsAlias = 'CollectIds'; + + const queryIdsQuery = this.microOrmUtilService + .queryBuilder(collectIdsAlias) + .select(this.microOrmUtilService.currentPrimaryColumn) + .join(paginationQuery, this.microOrmUtilService.currentAlias, { + [`${collectIdsAlias}.${this.microOrmUtilService.currentPrimaryColumn}`]: + this.microOrmUtilService + .getKnex() + .ref( + `${this.microOrmUtilService.currentAlias}.${this.microOrmUtilService.currentPrimaryColumn}` + ), + }); + + const queryCount = this.microOrmUtilService + .queryBuilder() + .from( + countSubQuery + .clone() + .select(this.microOrmUtilService.currentPrimaryColumn) + ) + .count(this.microOrmUtilService.currentPrimaryColumn, true); + + const resCount = await queryCount.execute('get'); + const count = resCount ? +resCount.count : 0; + + if (count === 0) { + return { + totalItems: count, + items: [], + }; + } + + const resIds = await queryIdsQuery + .distinct() + .setFlag(QueryFlag.DISABLE_PAGINATE) + .execute('all'); + + const idsArray = resIds.map( + (r) => r[this.microOrmUtilService.currentPrimaryColumn] + ); + const resultQueryBuilder = this.microOrmUtilService.queryBuilder().where({ + [this.microOrmUtilService.currentPrimaryColumn]: { + $in: idsArray, + }, + }); + + const sortObject = getSortObject(query); + + return { + totalItems: count, + items: await this.microOrmUtilService + .prePareQueryBuilder(resultQueryBuilder, query) + .orderBy( + Object.keys(sortObject).length > 0 + ? sortObject + : { + [this.microOrmUtilService.currentPrimaryColumn]: 'ASC', + } + ) + .execute('all', true), + }; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts new file mode 100644 index 00000000..1c434aad --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts @@ -0,0 +1,242 @@ +import { EntityManager, MikroORM } from '@mikro-orm/core'; +import { FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { + UserGroups, + Users, + pullAllData, + getDefaultQuery, + dbRandomName, + getModuleForPgLite, +} from '../../../../mock-utils/microrom'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { getQueryForCount } from './get-query-for-count'; +import { MicroOrmService } from '../../service'; + +describe('get-query-for-count', () => { + let mikroORMUserGroup: MikroORM; + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + beforeAll(async () => { + dbName = dbRandomName(true); + const moduleUserGroup = await getModuleForPgLite(UserGroups, dbName); + + mikroORMUserGroup = moduleUserGroup.get(MikroORM); + + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUserGroup.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullAllData(em); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUserGroup.close(true); + mikroORMUsers.close(true); + }); + + it('has only sort data', () => { + const query = getDefaultQuery(); + query.sort = { + target: { + id: 'ASC', + lastName: 'DESC', + }, + userGroup: { + id: 'ASC', + }, + roles: { + name: 'DESC', + }, + }; + + const result = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query]); + + expect(result.getFormattedQuery()).toBe( + `select "Users"."id" from "public"."users" as "Users" left join "public"."users_have_roles" as "u2" on "Users"."id" = "u2"."user_id" left join "public"."roles" as "u1" on "u2"."role_id" = "u1"."id" order by "Users"."id" asc, "Users"."last_name" desc, "Users"."user_groups_id" asc, "u1"."name" desc` + ); + }); + + it('has only filter data', () => { + const query = getDefaultQuery(); + query.filter = { + target: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + testReal: { + [FilterOperand.some]: ['test'], + }, + }, + }; + + const query1 = getDefaultQuery(); + query1.filter = { + target: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + relation: { + comments: { + kind: { + [FilterOperand.eq]: 'COMMENT', + }, + }, + userGroup: { + label: { + [FilterOperand.eq]: 'test', + }, + }, + }, + }; + + const query2 = getDefaultQuery(); + query2.filter = { + target: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + relation: { + manager: { + login: { + [FilterOperand.eq]: 'test', + }, + }, + addresses: { + city: { + [FilterOperand.eq]: 'test', + }, + }, + }, + }; + + const query3 = getDefaultQuery(); + query3.filter = { + target: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + relation: { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isDefault: { + [FilterOperand.eq]: 'false', + }, + }, + }, + }; + + const result = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query]); + + expect(result.getFormattedQuery()).toBe( + `select "Users"."id" from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and "Users"."test_real" && '{test}'` + ); + + const result1 = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query1]); + + expect(result1.getFormattedQuery()).toBe( + `select "Users"."id" from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and (exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id" and "Comments"."kind" = 'COMMENT')) and (exists (select 1 from "public"."user_groups" as "UserGroups" where "UserGroups"."id" = "Users"."user_groups_id" and "UserGroups"."label" = 'test'))` + ); + + const result2 = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query2]); + expect(result2.getFormattedQuery()).toBe( + `select "Users"."id" from "public"."users" as "Users" left join "public"."users" as "u1" on "Users"."manager_id" = "u1"."id" left join "public"."addresses" as "a2" on "Users"."addresses_id" = "a2"."id" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and "u1"."login" = 'test' and "a2"."city" = 'test'` + ); + + const result3 = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query3]); + expect(result3.getFormattedQuery()).toBe( + `select "Users"."id" from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and (exists (select 1 from "public"."users_have_roles" as "users_have_roles" left join "public"."roles" as "r1" on "users_have_roles"."role_id" = "r1"."id" where "users_have_roles"."user_id" = "Users"."id" and "r1"."key" = 'test' and "r1"."key" != 'test2' and "r1"."is_default" = 'false'))` + ); + }); + + it('has only filter data with sort', () => { + const query1 = getDefaultQuery(); + query1.filter = { + relation: { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isDefault: { + [FilterOperand.eq]: 'false', + }, + }, + }, + }; + query1.sort = { + target: { + id: 'ASC', + lastName: 'DESC', + }, + userGroup: { + id: 'ASC', + }, + roles: { + name: 'DESC', + }, + }; + + const result1 = getQueryForCount.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, ...[query1]); + expect(result1.getFormattedQuery()).toBe( + `select "Users"."id" from "public"."users" as "Users" left join "public"."users_have_roles" as "u2" on "Users"."id" = "u2"."user_id" left join "public"."roles" as "u1" on "u2"."role_id" = "u1"."id" where "u1"."key" = 'test' and "u1"."key" != 'test2' and "u1"."is_default" = 'false' order by "Users"."id" asc, "Users"."last_name" desc, "Users"."user_groups_id" asc, "u1"."name" desc` + ); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts new file mode 100644 index 00000000..8dbde638 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts @@ -0,0 +1,60 @@ +import { ObjectTyped, ResourceObject } from '@klerick/json-api-nestjs-shared'; + +import { MicroOrmService } from '../../service'; +import { Query } from '../../../mixin/zod'; +import { ObjectLiteral } from '../../../../types'; +import type { QBQueryOrderMap, EntityKey } from '@mikro-orm/core'; + +export function getSortObject( + query: Query +): QBQueryOrderMap { + const { sort } = query; + const sortObject: QBQueryOrderMap = {}; + if (!sort) return sortObject; + + const { target = {}, ...relation } = sort; + for (const [filed, sortType] of ObjectTyped.entries(target)) { + sortObject[filed] = sortType; + } + + for (const [relationName, orderConfig = {}] of ObjectTyped.entries( + relation + )) { + const name = relationName as unknown as EntityKey; + sortObject[name] = {}; + for (const [field, sortType] of ObjectTyped.entries(orderConfig)) { + sortObject[name][field] = sortType; + } + } + return sortObject; +} + +export function getQueryForCount( + this: MicroOrmService, + query: Query +) { + const querySelect = this.microOrmUtilService.queryBuilder(); + const sortObject = getSortObject(query); + querySelect.orderBy( + Object.keys(sortObject).length > 0 + ? sortObject + : { + [this.microOrmUtilService.currentPrimaryColumn]: 'ASC', + } + ); + + const expressionArrayForTarget = + this.microOrmUtilService.getFilterExpressionForTarget(query); + const expressionArrayForRelation = + this.microOrmUtilService.getFilterExpressionForRelation(query); + + const resultExpression = [ + ...expressionArrayForTarget, + ...expressionArrayForRelation, + ]; + for (const expression of resultExpression) { + querySelect.andWhere(expression); + } + + return querySelect; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts new file mode 100644 index 00000000..df09dce8 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts @@ -0,0 +1,102 @@ +import { EntityManager, MikroORM } from '@mikro-orm/core'; + +import { + dbRandomName, + getDefaultQuery, + getModuleForPgLite, + pullData, + Users, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { getOne } from './get-one'; +import { NotFoundException } from '@nestjs/common'; + +describe('get-one', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('Get one item', async () => { + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .limit(1) + .execute('get', true); + const query = getDefaultQuery(); + + const result = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, checkData.id, query); + + expect(JSON.stringify(result)).toBe(JSON.stringify(checkData)); + }); + + it('Get one item with select', async () => { + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .select(['id', 'firstName', 'isActive']) + .leftJoinAndSelect('Users.addresses', 'Addresses__addresses') + .leftJoinAndSelect('Users.comments', 'Comments__comments', {}, ['text']) + .leftJoinAndSelect('Users.manager', 'Users__manager', {}, ['login']) + .where({ + comments: { + $exists: true, + }, + }) + .limit(1) + .execute('get', true); + + const query = getDefaultQuery(); + query.include = ['addresses', 'comments', 'manager']; + query.fields = { + target: ['firstName', 'isActive'], + comments: ['text'], + manager: ['login'], + }; + const result = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, checkData.id, query); + + expect(JSON.stringify(result)).toBe(JSON.stringify(checkData)); + }); + it('Should be error', async () => { + expect.assertions(1); + const query = getDefaultQuery(); + try { + await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, 1000, query); + } catch (e) { + expect(e).toBeInstanceOf(NotFoundException); + } + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts new file mode 100644 index 00000000..124baa51 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts @@ -0,0 +1,29 @@ +import { NotFoundException } from '@nestjs/common'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { QueryOne } from '../../../mixin/zod'; +import { MicroOrmService } from '../../service'; + +export async function getOne( + this: MicroOrmService, + id: number | string, + query: QueryOne +): Promise { + const queryBuilder = this.microOrmUtilService.queryBuilder().where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }); + + const resultItem = await this.microOrmUtilService + .prePareQueryBuilder(queryBuilder, query) + .execute('get', true); + + if (!resultItem) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.microOrmUtilService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + + return resultItem; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts new file mode 100644 index 00000000..43958666 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts @@ -0,0 +1,144 @@ +import { + Collection, + EntityManager, + MikroORM, + NotFoundError, +} from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { getRelationship } from './get-relationship'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +describe('get-relationship', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups; + let notes: Collection; + let roles: Collection; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + const data = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, ['id']) + .leftJoinAndSelect('Users.comments', 'Comments_comments', {}, ['id']) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.notes', 'Notes__notes', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + addresses: { + $exists: true, + }, + roles: { + $exists: true, + }, + userGroup: { + $exists: true, + }, + }) + .getSingleResult(); + + if (!data) throw new Error(); + + ({ roles, notes, userGroup, addresses, comments, ...userObject as any } = + data); + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be ger result', async () => { + const { addresses: addressesResult } = await getRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'addresses'); + + expect(addressesResult.id).toBe(addresses.id); + + const { userGroup: userGroupResult } = await getRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup'); + + expect(userGroupResult.id).toBe(userGroup.id); + + const { roles: rolesResult } = await getRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles'); + for (const i of roles.map((i) => i.id)) { + expect(rolesResult.map((i) => i.id)).toContain(i); + } + }); + + it('should be error', async () => { + await expect( + getRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, '20000', 'roles') + ).rejects.toThrow(NotFoundException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts new file mode 100644 index 00000000..70bb7644 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts @@ -0,0 +1,34 @@ +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { NotFoundException } from '@nestjs/common'; + +import { MicroOrmService } from '../../service'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; + +export async function getRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>(this: MicroOrmService, id: number | string, rel: Rel): Promise { + const result = await this.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect( + `${this.microOrmUtilService.currentAlias}.${rel.toString()}`, + rel.toString(), + {}, + [this.microOrmUtilService.getPrimaryNameFor(rel)] + ) + .where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }) + .getSingleResult(); + + if (!result) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.microOrmUtilService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + + return result; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/index.ts new file mode 100644 index 00000000..9c1365d1 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/index.ts @@ -0,0 +1,9 @@ +export * from './get-all/get-all'; +export * from './get-one/get-one'; +export * from './delete-one/delete-one'; +export * from './post-one/post-one'; +export * from './patch-one/patch-one'; +export * from './get-relationship/get-relationship'; +export * from './delete-relationship/delete-relationship'; +export * from './post-relationship/post-relationship'; +export * from './patch-relationship/patch-relationship'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.spec.ts new file mode 100644 index 00000000..222ea525 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.spec.ts @@ -0,0 +1,234 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { patchOne } from './patch-one'; + +import { + BadRequestException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; + +describe('patch-one', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addresses: Addresses; + let addressForTest: Addresses; + let userGroup: UserGroups; + let notes: Collection; + let roles: Collection; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + const data = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, ['id']) + .leftJoinAndSelect('Users.comments', 'Comments_comments', {}, ['id']) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.notes', 'Notes__notes', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + addresses: { + $exists: true, + }, + roles: { + $exists: true, + }, + notes: { + $exists: true, + }, + userGroup: { + $exists: true, + }, + comments: { + $exists: true, + }, + }) + .limit(1) + .execute('get', true); + + ({ roles, notes, userGroup, addresses, comments, ...userObject as any } = + data); + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be update attr', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const result = await patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, userObject.id, { + id: userObject.id.toString(), + attributes: otherAttr, + type: 'users', + }); + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: userObject.id, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(result).toEqual(fromDb); + }); + + it('should be update relation', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const setRoles = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Roles) + .limit(2, 5) + .getResult(); + + const result = await patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, userObject.id, { + id: userObject.id.toString(), + attributes: otherAttr, + type: 'users', + relationships: { + addresses: { + data: { + id: addressForTest.id.toString(), + type: 'addresses', + }, + }, + comments: { + data: [], + }, + userGroup: { + data: null, + }, + roles: { + data: setRoles.map((i) => ({ id: i.id.toString(), type: 'roles' })), + }, + }, + }); + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: userObject.id, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(result).toEqual(fromDb); + }); + + it('should be error', async () => { + await expect( + patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, 1, { + attributes: {}, + type: 'users', + }) + ).rejects.toThrow(UnprocessableEntityException); + + await expect( + patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, 10000, { + id: '10000', + attributes: {}, + type: 'users', + }) + ).rejects.toThrow(NotFoundException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts new file mode 100644 index 00000000..44bd5bca --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts @@ -0,0 +1,52 @@ +import { + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { MicroOrmService } from '../../service'; +import { PatchData } from '../../../mixin/zod'; + +export async function patchOne( + this: MicroOrmService, + id: number | string, + inputData: PatchData +): Promise { + const { id: idBody, attributes, relationships } = inputData; + + if (`${id}` !== idBody) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Data 'id' must be equal to url param`, + path: ['data', 'id'], + }; + + throw new UnprocessableEntityException([error]); + } + + const existEntity = await this.microOrmUtilService + .queryBuilder() + .where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }) + .getSingleResult(); + + if (!existEntity) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.microOrmUtilService.currentAlias}' with id '${id}' does not exist`, + path: ['data', 'id'], + }; + throw new NotFoundException([error]); + } + + if (attributes) { + const attrTarget = this.microOrmUtilService.createEntity(attributes as any); + for (const [props, val] of ObjectTyped.entries(attrTarget)) { + existEntity[props] = val; + } + } + + return this.microOrmUtilService.saveEntity(existEntity, relationships); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts new file mode 100644 index 00000000..643dca0d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts @@ -0,0 +1,134 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { patchRelationship } from './patch-relationship'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +describe('patch-relationship', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups[]; + let notes: Collection; + let roles: Roles[]; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + userObject = (await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .limit(1) + .getSingleResult()) as Users; + + roles = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Roles) + .getResult(); + + userGroup = await microOrmServiceUser.microOrmUtilService + .queryBuilder(UserGroups) + .getResult(); + + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be ok', async () => { + const roles1 = faker.helpers.arrayElement(roles); + const roles2 = faker.helpers.arrayElement(roles); + const roles3 = faker.helpers.arrayElement(roles); + const userGroup1 = faker.helpers.arrayElement(userGroup); + const saveIdUserGroup = userGroup1.id; + await patchRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', [ + { type: 'roles', id: roles1.id.toString() }, + { type: 'roles', id: roles2.id.toString() }, + { type: 'roles', id: roles3.id.toString() }, + ]); + + await patchRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', { + type: 'user-groups', + id: saveIdUserGroup.toString(), + }); + + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + id: userObject.id, + }) + .getSingleResult(); + + expect(checkData?.roles.map((i) => i.id)).toEqual( + expect.arrayContaining([roles1.id, roles2.id, roles3.id]) + ); + + expect(checkData?.roles.map((i) => i.id)).toHaveLength( + [roles1.id, roles2.id, roles3.id].length + ); + expect(checkData?.userGroup.id).toBe(saveIdUserGroup); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts new file mode 100644 index 00000000..2a6d4b71 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts @@ -0,0 +1,52 @@ +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; +import { + PatchRelationshipData, + PostRelationshipData, +} from '../../../mixin/zod'; +import { getRelationship } from '../get-relationship/get-relationship'; +import { MicroOrmService } from '../../service'; + +export async function patchRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: MicroOrmService, + id: number | string, + rel: Rel, + input: PatchRelationshipData +): Promise { + const idsResult = await this.microOrmUtilService.validateRelationInputData( + rel, + input + ); + const currentEntityRef = this.microOrmUtilService.entityManager.getReference( + this.microOrmUtilService.entity, + id as any + ); + + const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); + + if (Array.isArray(idsResult)) { + const relRef = idsResult.map((i) => + this.microOrmUtilService.entityManager.getReference(relEntity, i as any) + ); + currentEntityRef[rel].removeAll(); + currentEntityRef[rel].add(...relRef); + } else { + // @ts-ignore + currentEntityRef[rel] = this.microOrmUtilService.entityManager.getReference( + relEntity, + idsResult as any + ); + } + + await this.microOrmUtilService.entityManager.flush(); + + return getRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.spec.ts new file mode 100644 index 00000000..9132f0d6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.spec.ts @@ -0,0 +1,338 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { postOne } from './post-one'; +import { BadRequestException } from '@nestjs/common'; + +describe('post-one', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups; + let notes: Collection; + let roles: Collection; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + const data = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses', {}, ['id']) + .leftJoinAndSelect('Users.comments', 'Comments_comments', {}, ['id']) + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.notes', 'Notes__notes', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + addresses: { + $exists: true, + }, + roles: { + $exists: true, + }, + notes: { + $exists: true, + }, + userGroup: { + $exists: true, + }, + comments: { + $exists: true, + }, + }) + .limit(1) + .execute('get', true); + + ({ roles, notes, userGroup, addresses, comments, ...userObject as any } = + data); + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('simple create', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const result = await postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + }); + + const { id: newId } = result; + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: newId, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(result).toEqual(fromDb); + }); + + it('simple create withId', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const result = await postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + id: id.toString(), + attributes: otherAttr, + type: 'users', + }); + + const { id: newId } = result; + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: newId, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(newId).toBe(id.toString()); + expect(result).toEqual(fromDb); + }); + + it('create with relation', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + const result = await postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + relationships: { + addresses: { + data: { + id: addressForTest.id.toString(), + type: 'addresses', + }, + }, + comments: { + data: comments.map((i) => ({ + id: i.id.toString(), + type: 'comments', + })), + }, + roles: { + data: roles.map((i) => ({ + id: i.id.toString(), + type: 'roles', + })), + }, + notes: { + data: notes.map((i) => ({ + id: i.id.toString(), + type: 'notes', + })), + }, + userGroup: { + data: { + id: userGroup.id.toString(), + type: 'user-groups', + }, + }, + manager: { + data: { + id: userObject.id.toString(), + type: 'users', + }, + }, + }, + }); + + const { id: newId } = result; + + const fromDb = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .where({ + id: newId, + }) + .leftJoinAndSelect('Users.addresses', 'Addresses_addresses') + .leftJoinAndSelect('Users.comments', 'Comments_comments') + .leftJoinAndSelect('Users.roles', 'Roles__roles') + .leftJoinAndSelect('Users.notes', 'Notes__notes') + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup') + .limit(1) + .getSingleResult(); + + expect(result).toEqual(fromDb); + }); + + it('should be error', async () => { + const { + id, + manager: mewManager, + userGroup: newUserGroup, + addresses: newAddresses, + comments: newComments, + notes: newNotes, + roles: newRoles, + ...otherAttr + } = newUser; + + await expect( + postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + relationships: { + addresses: { + data: { + id: '999999', + type: 'addresses', + }, + }, + }, + }) + ).rejects.toThrow(BadRequestException); + + await expect( + postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + relationships: { + roles: { + data: [ + { + id: '999999', + type: 'addresses', + }, + ], + }, + }, + }) + ).rejects.toThrow(BadRequestException); + + await expect( + postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(microOrmServiceUser, { + attributes: otherAttr, + type: 'users', + relationships: { + // @ts-expect-error check run time error + incorrectRel: { + data: [ + { + id: '999999', + type: 'addresses', + }, + ], + }, + }, + }) + ).rejects.toThrow(BadRequestException); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.ts new file mode 100644 index 00000000..239733c6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-one/post-one.ts @@ -0,0 +1,23 @@ +import { ObjectLiteral } from '../../../../types'; +import { MicroOrmService } from '../../service'; +import { PostData } from '../../../mixin/zod'; + +export async function postOne( + this: MicroOrmService, + inputData: PostData +): Promise { + const { attributes, relationships, id } = inputData; + + const idObject = id + ? { [this.microOrmUtilService.currentPrimaryColumn.toString()]: id } + : {}; + + const attributesObject = { + ...attributes, + ...idObject, + }; + + const entityIns = this.microOrmUtilService.createEntity(attributesObject); + + return this.microOrmUtilService.saveEntity(entityIns, relationships); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts new file mode 100644 index 00000000..bbbeb4c2 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts @@ -0,0 +1,147 @@ +import { Collection, EntityManager, MikroORM } from '@mikro-orm/core'; +import { faker } from '@faker-js/faker'; + +import { + Addresses, + dbRandomName, + getModuleForPgLite, + Notes, + pullData, + Roles, + UserGroups, + Users, + Comments, + pullAddress, +} from '../../../../mock-utils/microrom'; +import { MicroOrmService } from '../../service'; + +import { + CURRENT_ENTITY_MANAGER_TOKEN, + ORM_SERVICE, +} from '../../../../constants'; + +import { postRelationship } from './post-relationship'; +import { BadRequestException } from '@nestjs/common'; +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +describe('post-relationshipa', () => { + let mikroORMUsers: MikroORM; + let microOrmServiceUser: MicroOrmService; + let em: EntityManager; + let dbName: string; + let addressForTest: Addresses; + let addresses: Addresses; + let userGroup: UserGroups[]; + let notes: Collection; + let roles: Roles[]; + let comments: Collection; + let userObject: Users; + let newUser: Users; + beforeAll(async () => { + dbName = dbRandomName(); + const moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmServiceUser = moduleUsers.get>(ORM_SERVICE); + mikroORMUsers = moduleUsers.get(MikroORM); + em = moduleUsers.get(CURRENT_ENTITY_MANAGER_TOKEN); + await pullData(em, 10); + }); + + beforeEach(async () => { + userObject = (await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .limit(1) + .getSingleResult()) as Users; + + roles = await microOrmServiceUser.microOrmUtilService + .queryBuilder(Roles) + .getResult(); + + userGroup = await microOrmServiceUser.microOrmUtilService + .queryBuilder(UserGroups) + .getResult(); + + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + newUser = { + id: faker.number.int({ min: 0, max: 999999 }), + firstName: firstName, + lastName: lastName, + isActive: faker.datatype.boolean(), + login: faker.internet.userName({ + lastName: firstName, + firstName: lastName, + }), + testReal: [faker.number.float({ fractionDigits: 4 })], + testArrayNull: null, + testDate: faker.date.anytime(), + } as Users; + + addressForTest = await pullAddress(); + await em.persistAndFlush(addressForTest); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(() => { + mikroORMUsers.close(true); + }); + + it('should be ok', async () => { + const roles1 = faker.helpers.arrayElement(roles); + const roles2 = faker.helpers.arrayElement(roles); + const roles3 = faker.helpers.arrayElement(roles); + const userGroup1 = faker.helpers.arrayElement(userGroup); + const saveIdUserGroup = userGroup1.id; + + const checkDataBefore = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + id: userObject.id, + }) + .getSingleResult(); + + const saveRolesIds = checkDataBefore?.roles.map((i) => i.id) || []; + + await postRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'roles', [ + { type: 'roles', id: roles1.id.toString() }, + { type: 'roles', id: roles2.id.toString() }, + { type: 'roles', id: roles3.id.toString() }, + ]); + + await postRelationship.call< + MicroOrmService, + Parameters>>, + ReturnType>> + >(microOrmServiceUser, userObject.id, 'userGroup', { + type: 'user-groups', + id: saveIdUserGroup.toString(), + }); + + const checkData = await microOrmServiceUser.microOrmUtilService + .queryBuilder() + .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['id']) + .leftJoinAndSelect('Users.userGroup', 'UserGroups__userGroup', {}, ['id']) + .where({ + id: userObject.id, + }) + .getSingleResult(); + + expect(checkData?.roles.map((i) => i.id)).toEqual( + expect.arrayContaining([roles1.id, roles2.id, roles3.id]) + ); + + expect(checkData?.roles.map((i) => i.id)).toHaveLength( + [roles1.id, roles2.id, roles3.id].length + saveRolesIds.length + ); + expect(checkData?.userGroup.id).toBe(saveIdUserGroup); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts new file mode 100644 index 00000000..5462417c --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts @@ -0,0 +1,48 @@ +import { EntityRelation } from '@klerick/json-api-nestjs-shared'; + +import { ObjectLiteral } from '../../../../types'; +import { PostRelationshipData } from '../../../mixin/zod'; +import { getRelationship } from '../get-relationship/get-relationship'; +import { MicroOrmService } from '../../service'; + +export async function postRelationship< + E extends ObjectLiteral, + Rel extends EntityRelation +>( + this: MicroOrmService, + id: number | string, + rel: Rel, + input: PostRelationshipData +): Promise { + const idsResult = await this.microOrmUtilService.validateRelationInputData( + rel, + input + ); + const currentEntityRef = this.microOrmUtilService.entityManager.getReference( + this.microOrmUtilService.entity, + id as any + ); + + const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); + + if (Array.isArray(idsResult)) { + const relRef = idsResult.map((i) => + this.microOrmUtilService.entityManager.getReference(relEntity, i as any) + ); + currentEntityRef[rel].add(...relRef); + } else { + // @ts-ignore + currentEntityRef[rel] = this.microOrmUtilService.entityManager.getReference( + relEntity, + idsResult as any + ); + } + + await this.microOrmUtilService.entityManager.flush(); + + return getRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts new file mode 100644 index 00000000..679c16d9 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts @@ -0,0 +1,571 @@ +import { FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { MikroORM, RawQueryFragment } from '@mikro-orm/core'; +import { TestingModule } from '@nestjs/testing/testing-module'; + +import { MicroOrmUtilService } from './micro-orm-util.service'; + +import { + dbRandomName, + getDefaultQuery, + getModuleForPgLite, + Roles, + UserGroups, + Users, +} from '../../../mock-utils/microrom'; + +describe('MicroOrmUtilService', () => { + let mikroORMUserGroup: MikroORM; + let mikroORMUsers: MikroORM; + let mikroORMRoles: MikroORM; + let microOrmUtilsServiceUserGroups: MicroOrmUtilService; + let microOrmUtilsServiceUsers: MicroOrmUtilService; + let microOrmUtilsServiceRoles: MicroOrmUtilService; + let moduleRoles: TestingModule; + let moduleUsers: TestingModule; + let moduleUserGroup: TestingModule; + + let dbName: string; + + beforeAll(async () => { + dbName = dbRandomName(true); + moduleUserGroup = await getModuleForPgLite(UserGroups, dbName); + + microOrmUtilsServiceUserGroups = + moduleUserGroup.get>(MicroOrmUtilService); + mikroORMUserGroup = moduleUserGroup.get(MikroORM); + + moduleUsers = await getModuleForPgLite(Users, dbName); + microOrmUtilsServiceUsers = + moduleUsers.get>(MicroOrmUtilService); + + mikroORMUsers = moduleUsers.get(MikroORM); + + moduleRoles = await getModuleForPgLite(Roles, dbName); + microOrmUtilsServiceRoles = + moduleRoles.get>(MicroOrmUtilService); + mikroORMRoles = moduleRoles.get(MikroORM); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + afterAll(async () => { + await mikroORMUserGroup.close(true); + await mikroORMUsers.close(true); + await mikroORMRoles.close(true); + }); + + it('currentAlias', () => { + expect(microOrmUtilsServiceUserGroups.currentAlias).toBe('UserGroups'); + }); + + it('currentPrimaryColumn', () => { + expect(microOrmUtilsServiceUserGroups.currentPrimaryColumn).toBe('id'); + }); + + it('getAliasForEntity relation', () => { + expect(() => + microOrmUtilsServiceUsers.getAliasForPivotTable(Users, 'login') + ).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('relation not found'), + }) + ); + + expect(() => + microOrmUtilsServiceUsers.getAliasForPivotTable(Users, 'notes') + ).toThrowError( + expect.objectContaining({ + message: expect.stringContaining('Many to many relation expected'), + }) + ); + + expect( + microOrmUtilsServiceUsers.getAliasForPivotTable(Users, 'roles') + ).toBe('users_have_roles'); + }); + + it('queryBuilder', () => { + const resultUserGroups = microOrmUtilsServiceUserGroups.queryBuilder(); + expect(resultUserGroups.mainAlias.aliasName).toBe('UserGroups'); + expect(resultUserGroups.mainAlias.entityName).toBe('UserGroups'); + + const resultUsers = microOrmUtilsServiceUserGroups.queryBuilder(Users); + expect(resultUsers.mainAlias.aliasName).toBe('Users'); + expect(resultUsers.mainAlias.entityName).toBe('Users'); + + const resultTestUsersAlias = microOrmUtilsServiceUserGroups.queryBuilder( + Users, + 'TestUsers' + ); + expect(resultTestUsersAlias.mainAlias.entityName).toBe('Users'); + expect(resultTestUsersAlias.mainAlias.aliasName).toBe('TestUsers'); + + const resultTestUsersOnlyAlias = + microOrmUtilsServiceUserGroups.queryBuilder('TestUserGroups'); + expect(resultTestUsersOnlyAlias.mainAlias.entityName).toBe('UserGroups'); + expect(resultTestUsersOnlyAlias.mainAlias.aliasName).toBe('TestUserGroups'); + }); + + it('defaultOrder', () => { + expect(microOrmUtilsServiceUserGroups.defaultOrder).toEqual({ id: 'ASC' }); + }); + describe('getFilterExpressionForTarget', () => { + it('expression for target field, target is null, should be null expression array', () => { + const query = getDefaultQuery(); + const result = + microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(result).toEqual([]); + }); + it('expression for target field with target field', async () => { + const nullableField = 'id'; + const nullableFieldValue = null; + const notNullableField = 'login'; + const notNullableFieldValue = null; + const regexpField = 'firstName'; + const regexpFieldValue = 'firstName'; + + const otherFiled = 'lastName'; + const otherFiledValue1: [string, string, string] = ['1', '3', '4']; + const otherFiledValue2 = 'test'; + const otherFiledValue3 = 'test2'; + + const arrayField = 'testReal'; + const query = getDefaultQuery(); + query.filter.target = { + [nullableField]: { + [FilterOperand.eq]: nullableFieldValue, + }, + [notNullableField]: { + [FilterOperand.ne]: notNullableFieldValue, + }, + [regexpField]: { + [FilterOperand.regexp]: regexpFieldValue, + }, + [otherFiled]: { + [FilterOperand.in]: otherFiledValue1, + [FilterOperand.nin]: otherFiledValue1, + [FilterOperand.like]: otherFiledValue2, + [FilterOperand.gt]: otherFiledValue3, + }, + [arrayField]: { + [FilterOperand.some]: otherFiledValue1, + }, + }; + + const [ + id, + login, + regexpFieldConditional, + otherFiledConditional, + arrayFieldConditional, + ] = microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(id).toEqual({ + [nullableField]: { + ['$' + FilterOperand.eq]: nullableFieldValue, + }, + }); + + expect(login).toEqual({ + [notNullableField]: { + ['$' + FilterOperand.ne]: notNullableFieldValue, + }, + }); + + expect(regexpFieldConditional).toEqual({ + [regexpField]: { + ['$re']: regexpFieldValue, + }, + }); + expect(otherFiledConditional).toEqual({ + [otherFiled]: { + ['$' + FilterOperand.in]: otherFiledValue1, + ['$' + FilterOperand.nin]: otherFiledValue1, + ['$' + FilterOperand.like]: otherFiledValue2, + ['$' + FilterOperand.gt]: otherFiledValue3, + }, + }); + expect(arrayFieldConditional).toEqual({ + [arrayField]: { + ['$overlap']: otherFiledValue1, + }, + }); + }); + + it('expression for target field with relation field not exist', async () => { + const oneToOneToMyself = 'manager'; + const oneToOneToOther = 'addresses'; + const manyToMany = 'roles'; + const oneToMany = 'comments'; + const manyToOne = 'userGroup'; + + const query = getDefaultQuery(); + query.filter.target = { + [oneToOneToMyself]: { + [FilterOperand.eq]: 'null', + }, + [oneToOneToOther]: { + [FilterOperand.eq]: 'null', + }, + [manyToMany]: { + [FilterOperand.eq]: 'null', + }, + [oneToMany]: { + [FilterOperand.eq]: 'null', + }, + [manyToOne]: { + [FilterOperand.eq]: 'null', + }, + }; + + const [ + oneToOneToMyselfEq, + oneToOneToOtherEq, + manyToManyEq, + oneToManyEq, + manyToOneEq, + ] = microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(oneToOneToMyselfEq).toEqual({ + [oneToOneToMyself]: { $exists: false }, + }); + expect(oneToOneToOtherEq).toEqual({ + [oneToOneToOther]: { $exists: false }, + }); + expect(manyToOneEq).toEqual({ [manyToOne]: { $exists: false } }); + expect(manyToManyEq).toBeInstanceOf(RawQueryFragment); + expect(oneToManyEq).toBeInstanceOf(RawQueryFragment); + if (!(oneToManyEq instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + if (!(manyToManyEq instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(manyToManyEq.sql).toBe( + 'not exists (select 1 from "public"."users_have_roles" as "users_have_roles" where "users_have_roles"."user_id" = "Users"."id")' + ); + expect(oneToManyEq.sql).toBe( + 'not exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id")' + ); + }); + it('expression for target field with relation field exist', async () => { + const oneToOneToMyself = 'manager'; + const oneToOneToOther = 'addresses'; + const manyToMany = 'roles'; + const oneToMany = 'comments'; + const manyToOne = 'userGroup'; + + const query = getDefaultQuery(); + query.filter.target = { + [oneToOneToMyself]: { + [FilterOperand.ne]: 'null', + }, + [oneToOneToOther]: { + [FilterOperand.ne]: 'null', + }, + [manyToMany]: { + [FilterOperand.ne]: 'null', + }, + [oneToMany]: { + [FilterOperand.ne]: 'null', + }, + [manyToOne]: { + [FilterOperand.ne]: 'null', + }, + }; + + const [ + oneToOneToMyselfNe, + oneToOneToOtherNe, + manyToManyNe, + oneToManyNe, + manyToOneNe, + ] = microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(oneToOneToMyselfNe).toEqual({ + [oneToOneToMyself]: { $exists: true }, + }); + expect(oneToOneToOtherNe).toEqual({ + [oneToOneToOther]: { $exists: true }, + }); + expect(manyToOneNe).toEqual({ [manyToOne]: { $exists: true } }); + expect(manyToManyNe).toBeInstanceOf(RawQueryFragment); + expect(oneToManyNe).toBeInstanceOf(RawQueryFragment); + if (!(oneToManyNe instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + if (!(manyToManyNe instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(manyToManyNe.sql).toBe( + 'exists (select 1 from "public"."users_have_roles" as "users_have_roles" where "users_have_roles"."user_id" = "Users"."id")' + ); + expect(oneToManyNe.sql).toBe( + 'exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id")' + ); + }); + + it('expression for target field with relation field exist with sort and include', async () => { + const oneToOneToMyself = 'manager'; + const oneToOneToOther = 'addresses'; + const manyToMany = 'roles'; + const oneToMany = 'comments'; + const manyToOne = 'userGroup'; + + const query = getDefaultQuery(); + query.filter.target = { + [oneToOneToMyself]: { + [FilterOperand.ne]: 'null', + }, + [oneToOneToOther]: { + [FilterOperand.ne]: 'null', + }, + [manyToMany]: { + [FilterOperand.ne]: 'null', + }, + [oneToMany]: { + [FilterOperand.ne]: 'null', + }, + [manyToOne]: { + [FilterOperand.ne]: 'null', + }, + }; + + query.sort = { + [manyToMany]: { key: 'ASC' }, + }; + query.include = [oneToMany]; + + const [ + oneToOneToMyselfNe, + oneToOneToOtherNe, + manyToManyNe, + oneToManyNe, + manyToOneNe, + ] = microOrmUtilsServiceUsers.getFilterExpressionForTarget(query); + + expect(oneToOneToMyselfNe).toEqual({ + [oneToOneToMyself]: { $exists: true }, + }); + expect(oneToOneToOtherNe).toEqual({ + [oneToOneToOther]: { $exists: true }, + }); + expect(manyToOneNe).toEqual({ [manyToOne]: { $exists: true } }); + expect(manyToManyNe).toEqual({ roles: { $exists: true } }); + expect(oneToManyNe).toEqual({ comments: { $exists: true } }); + }); + }); + + describe('getFilterExpressionForRelation', () => { + it('expression for relation field OneToMany and ManyToOne', async () => { + const query = getDefaultQuery(); + + query.filter.relation = { + comments: { + kind: { + [FilterOperand.eq]: 'test', + }, + }, + userGroup: { + label: { + [FilterOperand.eq]: 'test', + }, + }, + }; + + const [comments, userGroup] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + + if (!(userGroup instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(userGroup.sql).toBe( + `exists (select 1 from "public"."user_groups" as "UserGroups" where "UserGroups"."id" = "Users"."user_groups_id" and "UserGroups"."label" = 'test')` + ); + + if (!(comments instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(comments.sql).toBe( + `exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id" and "Comments"."kind" = 'test')` + ); + }); + + it('expression for relation field OneToMany and ManyToOne with sort', async () => { + const query = getDefaultQuery(); + query.sort = { + userGroup: { + label: 'ASC', + }, + comments: { + kind: 'ASC', + }, + }; + query.filter.relation = { + comments: { + kind: { + [FilterOperand.eq]: 'test', + }, + }, + userGroup: { + label: { + [FilterOperand.eq]: 'test', + }, + }, + }; + + const [comments, userGroup] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + + expect(userGroup).toEqual({ userGroup: { label: { $eq: 'test' } } }); + + expect(comments).toEqual({ comments: { kind: { $eq: 'test' } } }); + }); + + it('expression for relation field OneToOne', async () => { + const query = getDefaultQuery(); + + query.filter.relation = { + manager: { + login: { + [FilterOperand.eq]: 'test', + }, + }, + addresses: { + city: { + [FilterOperand.eq]: 'test', + }, + }, + }; + const [manager, addresses] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + expect(manager).toEqual({ manager: { login: { $eq: 'test' } } }); + expect(addresses).toEqual({ addresses: { city: { $eq: 'test' } } }); + }); + + it('expression for relation field ManyToMany', async () => { + const query = getDefaultQuery(); + const queryRoles = getDefaultQuery(); + + query.filter.relation = { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isDefault: { + [FilterOperand.eq]: 'false', + }, + }, + }; + + queryRoles.filter.relation = { + users: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + }; + + const [roles] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + + if (!(roles instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + + expect(roles.sql).toBe( + `exists (select 1 from "public"."users_have_roles" as "users_have_roles" left join "public"."roles" as "r1" on "users_have_roles"."role_id" = "r1"."id" where "users_have_roles"."user_id" = "Users"."id" and "r1"."key" = 'test' and "r1"."key" != 'test2' and "r1"."is_default" = 'false')` + ); + + const [users] = + microOrmUtilsServiceRoles.getFilterExpressionForRelation(queryRoles); + + if (!(users instanceof RawQueryFragment)) + throw new Error('Is not RawQueryFragment'); + expect(users.sql).toBe( + `exists (select 1 from "public"."users_have_roles" as "users_have_roles" left join "public"."users" as "u1" on "users_have_roles"."user_id" = "u1"."id" where "users_have_roles"."role_id" = "Roles"."id" and "u1"."login" = 'test' and "u1"."login" != 'test2' and "u1"."is_active" = 'false')` + ); + }); + + it('expression for relation field ManyToMany with sort', async () => { + const query = getDefaultQuery(); + const queryRoles = getDefaultQuery(); + + query.filter.relation = { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isDefault: { + [FilterOperand.eq]: 'false', + }, + }, + }; + query.sort = { + roles: { key: 'ASC' }, + }; + + queryRoles.filter.relation = { + users: { + login: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + isActive: { + [FilterOperand.eq]: 'false', + }, + }, + }; + + queryRoles.sort = { + users: { login: 'ASC' }, + }; + + const [roles] = + microOrmUtilsServiceUsers.getFilterExpressionForRelation(query); + + expect(roles).toEqual({ + roles: { + isDefault: { $eq: 'false' }, + key: { $eq: 'test', $ne: 'test2' }, + }, + }); + + const [users] = + microOrmUtilsServiceRoles.getFilterExpressionForRelation(queryRoles); + + expect(users).toEqual({ + users: { + isActive: { $eq: 'false' }, + login: { $eq: 'test', $ne: 'test2' }, + }, + }); + }); + + it('getConditionalForJoin', () => { + const query = getDefaultQuery(); + query.filter.relation = { + roles: { + key: { + [FilterOperand.eq]: 'test', + [FilterOperand.ne]: 'test2', + }, + }, + }; + const result = microOrmUtilsServiceRoles.getConditionalForJoin( + query, + 'roles' + ); + expect(result).toEqual({ + key: { $eq: 'test', $ne: 'test2' }, + }); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts new file mode 100644 index 00000000..10b0a56b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts @@ -0,0 +1,726 @@ +import { + BadRequestException, + Inject, + Injectable, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { + EntityClass, + EntityKey, + EntityProperty, + EntityRepository, + type QBFilterQuery, + QBQueryOrderMap, + raw, + ReferenceKind, + EntityData, + Collection, +} from '@mikro-orm/core'; +import { + Knex, + SqlEntityManager, + QueryBuilder, + type Field, +} from '@mikro-orm/knex'; +import { + camelToKebab, + EntityRelation, + FilterOperand, + ObjectTyped, +} from '@klerick/json-api-nestjs-shared'; + +import { + ASC, + CURRENT_ENTITY, + CURRENT_ENTITY_MANAGER_TOKEN, + CURRENT_ENTITY_REPOSITORY, +} from '../../../constants'; + +import { ObjectLiteral, ValidateQueryError } from '../../../types'; +import { Query, QueryOne, Relationships } from '../../mixin/zod'; +import { In } from 'typeorm'; +import { TupleOfEntityRelation } from '../../mixin/types'; +import { InputValidateData, ValidateReturn } from '../../type-orm/service'; + +type RelationshipsResult = { + [K in EntityRelation]: E[K] extends Collection ? E[K] : E[K] | null; +}; + +function isRelationField( + relationField: string[], + field: any +): asserts field is EntityKey { + if (relationField.includes(field)) return; + const error: ValidateQueryError = { + code: 'unrecognized_keys', + path: ['data', 'relationships'], + message: `Resource for relation '${field.toString()}' does not exist`, + keys: [field], + }; + + throw new BadRequestException([error]); +} +const getErrorObject = ( + props: string, + message: string +): ValidateQueryError => ({ + code: 'invalid_arguments', + path: ['data', 'relationships'], + message, +}); + +Injectable(); +export class MicroOrmUtilService { + @Inject(CURRENT_ENTITY_MANAGER_TOKEN) + public readonly entityManager!: SqlEntityManager; + @Inject(CURRENT_ENTITY_REPOSITORY) + private entityRepository!: EntityRepository; + @Inject(CURRENT_ENTITY) public readonly entity!: EntityClass; + + private _relationsName!: EntityKey[]; + private _relationMap = new Map, EntityProperty>(); + get relationsName() { + if (!this._relationsName) { + this._relationsName = this.metadata.relations.map((r) => r.name); + } + + return this._relationsName; + } + + getRelation(name: EntityKey) { + let relation = this._relationMap.get(name); + if (!relation) { + relation = this.metadata.relations.find((r) => r.name === name); + if (!relation) { + throw new Error(`Relation ${name} not found in ${this.metadata.name}`); + } + this._relationMap.set(name, relation); + } + + return relation; + } + + get currentAlias() { + return this.entityRepository.getEntityName(); + } + + get metadata() { + return this.entityManager.getMetadata().get(this.entity); + } + + get currentPrimaryColumn() { + return this.metadata.getPrimaryProp().name; + } + + get defaultOrder(): QBQueryOrderMap { + return { + [this.currentPrimaryColumn]: ASC, + }; + } + + getAliasForEntity(entity: EntityClass) { + return this.entityManager.getRepository(entity).getEntityName(); + } + + getAliasForPivotTable(relName: keyof T): string; + getAliasForPivotTable( + entity: EntityClass, + relName: keyof T + ): string; + getAliasForPivotTable( + entity: EntityClass | keyof T, + relName?: keyof T + ): string { + if (!relName) { + relName = entity as keyof T; + entity = this.entity; + } else { + entity = entity as EntityClass; + } + + const propsRelation = this.entityManager + .getMetadata() + .get(entity) + .relations.find((r) => r.name.toString() === relName.toString()); + if (!propsRelation) + throw new Error(`${relName.toString()} relation not found`); + if (propsRelation.kind !== ReferenceKind.MANY_TO_MANY) + throw new Error('Many to many relation expected'); + + return this.entityManager + .getRepository(propsRelation.pivotEntity) + .getEntityName(); + } + + queryBuilder( + entity: EntityClass, + alias: string + ): QueryBuilder; + queryBuilder( + entity: EntityClass + ): QueryBuilder; + queryBuilder( + alias: string + ): QueryBuilder; + queryBuilder(): QueryBuilder; + queryBuilder( + ...arg: + | [entity: EntityClass, alias: string] + | [entity: EntityClass] + | [alias: string] + | [undefined, undefined] + ): QueryBuilder { + let [entity, alias] = arg; + + if (entity && !alias) { + if (typeof entity === 'string') { + alias = entity; + entity = this.entity; + } else { + alias = this.getAliasForEntity(entity); + } + } + if (!entity) { + alias = this.currentAlias; + entity = this.entity; + } + + if (!entity || !alias) { + throw new Error('entity or alias not found'); + } + + return this.entityManager.createQueryBuilder( + entity, + alias + ); + } + + getFilterExpressionForTarget( + query: Query + ): QBFilterQuery[] { + const result: QBFilterQuery[] = []; + const filterTarget = this.getFilterObject(query, 'target'); + const { sort, include } = query; + if (!filterTarget) return result; + + for (const [fieldName, filter] of ObjectTyped.entries(filterTarget)) { + const tmpField = fieldName as unknown as EntityKey; + + if (filter === undefined) continue; + const filterObject: QBFilterQuery = { + [tmpField]: {}, + }; + let subQueryExpression: QBFilterQuery | undefined; + for (const entries of ObjectTyped.entries(filter)) { + const [operand, value] = entries as [FilterOperand, string]; + + if (!this.relationsName.includes(tmpField)) { + const operandForMiroOrmResult = this.extractedResultOperand(operand); + filterObject[tmpField.toString()][operandForMiroOrmResult] = value; + continue; + } + + const relation = this.getRelation(tmpField); + switch (relation.kind) { + case ReferenceKind.MANY_TO_MANY: + case ReferenceKind.ONE_TO_MANY: { + if (sort && tmpField in sort) { + filterObject[tmpField.toString()]['$exists'] = + operand === FilterOperand.ne; + break; + } + + if (include && include.includes(tmpField)) { + filterObject[tmpField.toString()]['$exists'] = + operand === FilterOperand.ne; + break; + } + + const subQuery = + this.getSubQueryForRelation(tmpField).getFormattedQuery(); + const type = operand === FilterOperand.ne ? 'exists' : 'not exists'; + const resultQuery = `${type} (${subQuery})`; + subQueryExpression = raw(resultQuery); + break; + } + default: + filterObject[tmpField.toString()]['$exists'] = + operand === FilterOperand.ne; + } + } + + result.push(subQueryExpression ? subQueryExpression : filterObject); + } + + return result; + } + + getConditionalForJoin( + query: Query, + key: string + ): QBFilterQuery { + const filterRelation = this.getFilterObject(query, 'relation'); + + if (!filterRelation) return {}; + if (!(key in filterRelation)) return {}; + + for (const [key, reletionConditional] of ObjectTyped.entries( + filterRelation + )) { + if (key !== key) continue; + if (!reletionConditional) continue; + + for (const [field, conditional] of ObjectTyped.entries( + reletionConditional + )) { + if (!conditional) continue; + return Object.entries(conditional).reduce((acum, [operand, value]) => { + acum[field.toString()] = { + ...(acum[field.toString()] || {}), + [this.extractedResultOperand(operand as FilterOperand)]: value, + }; + return acum; + }, {} as QBFilterQuery); + } + } + + return {}; + } + + private extractedResultOperand(operand: FilterOperand) { + return operand === 'regexp' + ? '$re' + : operand === 'some' + ? '$overlap' + : (('$' + operand) as `$${FilterOperand}`); + } + + getFilterExpressionForRelation( + query: Query + ): QBFilterQuery[] { + const result: QBFilterQuery[] = []; + const filterRelation = this.getFilterObject(query, 'relation'); + const sort = query.sort; + if (!filterRelation) return result; + + for (const [relationField, propsFilter] of ObjectTyped.entries( + filterRelation + )) { + const fieldName = relationField as unknown as EntityKey; + const relationProps = this.getRelation(fieldName); + if (!propsFilter) continue; + if (!this.relationsName.includes(fieldName)) continue; + const filterObject: QBFilterQuery = { + [relationField]: {}, + }; + let subQueryExpression: + | ReturnType + | undefined; + for (const [relationFieldProps, filter] of ObjectTyped.entries( + propsFilter + )) { + const fieldProps = relationFieldProps as unknown as EntityKey; + if (!filter) continue; + for (const entries of ObjectTyped.entries(filter)) { + const [operand, value] = entries as [FilterOperand, string]; + switch (relationProps.kind) { + case ReferenceKind.MANY_TO_MANY: + case ReferenceKind.ONE_TO_MANY: + case ReferenceKind.MANY_TO_ONE: + { + if (sort && relationField in sort) { + filterObject[fieldName][fieldProps] = filterObject[fieldName][ + fieldProps + ] = filterObject[fieldName][fieldProps] || {}; + filterObject[fieldName][fieldProps][ + this.extractedResultOperand(operand) + ] = value; + } else { + if (!subQueryExpression) { + subQueryExpression = this.getSubQueryForRelation(fieldName); + } + const expression = + relationProps.kind === ReferenceKind.MANY_TO_MANY + ? { + [this.getInverseFieldForManyToMany(fieldName)]: { + [fieldProps]: { + [this.extractedResultOperand(operand)]: value, + }, + }, + } + : { + [fieldProps]: { + [this.extractedResultOperand(operand)]: value, + }, + }; + subQueryExpression.where(expression, '$and'); + } + } + break; + default: + filterObject[fieldName][fieldProps] = + filterObject[fieldName][fieldProps] || {}; + filterObject[fieldName][fieldProps][ + this.extractedResultOperand(operand) + ] = value; + } + } + } + if (subQueryExpression) { + const resultQuery = `exists (${subQueryExpression.getFormattedQuery()})`; + result.push(raw(resultQuery)); + subQueryExpression = undefined; + } else { + result.push(filterObject); + } + } + + return result; + } + + getKnex(): Knex { + return this.entityManager.getKnex(); + } + + prePareQueryBuilder( + queryBuilder: QueryBuilder, + query: Query | QueryOne + ): QueryBuilder { + const { fields, include } = query; + const relationFields: Record = {}; + + if (fields) { + const { target, ...relations } = fields; + Object.assign(relationFields, relations); + if (target) { + if (!target.includes(this.currentPrimaryColumn)) { + target.unshift(this.currentPrimaryColumn); + } + queryBuilder.select(target as unknown as Field); + } + } + + const resultInclude = new Set([ + ...[...(include || []), ...ObjectTyped.keys(relationFields)], + ]); + + for (const item of resultInclude) { + const relationProps = this.getRelation( + item as unknown as EntityKey + ); + const relationEntity = relationProps.entity() as EntityClass; + const relationAlias = this.getAliasForEntity(relationEntity); + const mainAlias = this.currentAlias; + + const condition: QBFilterQuery = this.getConditionalForJoin( + query as Query, + item + ); + + let selectJoin: undefined | string[] = undefined; + if (item in relationFields) { + const tmpSet = new Set([ + ...relationFields[item], + this.getPrimaryNameFor(item as any), + ]); + + selectJoin = [...tmpSet]; + } + + queryBuilder.leftJoinAndSelect( + `${mainAlias}.${item}`, + `${relationAlias}__${item}`, + condition, + selectJoin + ); + } + return queryBuilder; + } + + getPrimaryNameFor(rel: EntityRelation) { + const relationEntity = this.getRelation( + rel as unknown as EntityKey + ).entity() as EntityClass; + return this.entityManager.getMetadata().get(relationEntity).getPrimaryProp() + .name; + } + + private getFilterObject( + query: Query, + filterType: 'target' | 'relation' + ) { + const { filter } = query; + if (!filter) return null; + return filter[filterType]; + } + + private getSubQueryForRelation(propsName: EntityKey) { + const relation = this.getRelation(propsName); + + let pivotTableName; + let filedCheck: string | undefined; + let expressionColumnName: string | undefined = this.currentPrimaryColumn; + if (relation.kind === ReferenceKind.MANY_TO_MANY) { + pivotTableName = this.getAliasForPivotTable(propsName); + filedCheck = relation.joinColumns.at(0); + } else if (relation.kind === ReferenceKind.ONE_TO_MANY) { + pivotTableName = this.getAliasForEntity(relation.entity() as any); + filedCheck = relation.mappedBy; + } else { + expressionColumnName = relation.joinColumns.at(0); + pivotTableName = this.getAliasForEntity(relation.entity() as any); + filedCheck = relation.referencedColumnNames.at(0); + } + + if (!filedCheck) throw new Error('filedCheck not found'); + if (!expressionColumnName) + throw new Error('expressionColumnName not found'); + + return this.entityManager + .createQueryBuilder(pivotTableName, pivotTableName) + .select(raw('1')) + .from(pivotTableName) + .where({ + [filedCheck]: this.entityManager + .getKnex() + .ref(`${this.currentAlias}.${expressionColumnName}`), + }); + } + + private getInverseFieldForManyToMany(propsName: EntityKey) { + const relation = this.getRelation(propsName); + const pivotTableName = this.getAliasForPivotTable(propsName); + + const pivotMetaData = this.entityManager.getMetadata().get(pivotTableName); + + const props = pivotMetaData.props.find( + (prop) => + prop.targetMeta && + prop.targetMeta.properties[propsName] && + prop.targetMeta.properties[propsName].entity() === relation.entity() + ); + + if (!props) + throw new Error( + `ManyToMany relation ${propsName} not found in ${pivotTableName}` + ); + + return props.inversedBy || props.mappedBy; + } + + createEntity(params: EntityData) { + return this.entityManager.create(this.entity, params, { + partial: true, + persist: false, + }); + } + + private async *asyncIterateFindRelationships( + relationships: NonNullable> + ): AsyncGenerator> { + for (const entries of ObjectTyped.entries(relationships)) { + const [props, dataItem] = entries; + isRelationField(this.relationsName, props); + const propsKey = props as unknown as EntityKey; + if (dataItem === undefined) continue; + + const { data } = dataItem; + if (data === undefined) continue; + if (data === null) { + yield { [props]: null } as RelationshipsResult; + continue; + } + const isArray = Array.isArray(data); + if (isArray && data.length === 0) { + yield { [props]: [] } as RelationshipsResult; + continue; + } + + const condition = isArray + ? { + $in: data.map((i) => (i || {}).id), + } + : { + $eq: data['id'], + }; + + const relationProps = this.metadata.properties[propsKey]; + const relationEntity = relationProps.entity() as EntityClass; + const metadata = this.entityManager.getMetadata().get(relationEntity); + const primaryName = metadata.getPrimaryProp().name; + + const queryBuilder = this.queryBuilder( + relationEntity, + this.getAliasForEntity(relationEntity) + ) + .select([primaryName]) + .where({ + [primaryName]: condition, + }); + let result: typeof relationEntity | (typeof relationEntity)[]; + let error: BadRequestException | undefined = undefined; + if (isArray) { + const tmpResult = await queryBuilder.getResult(); + + if (tmpResult.length === 0 || data.length !== tmpResult.length) { + const msg = `Resource '${metadata.className}' with ids '${data + .map((i) => (i || {})['id']) + .filter((i) => !tmpResult.find((r) => r[primaryName] == i)) + .join(',')}' does not exist`; + error = new BadRequestException([getErrorObject(props, msg)]); + } + result = tmpResult; + } else { + const tmpResult = await queryBuilder.getSingleResult(); + if (!tmpResult) + error = new BadRequestException([ + getErrorObject( + props, + `Resource '${metadata.className}' with id '${data.id}' does not exist` + ), + ]); + result = tmpResult; + } + if (error) throw error; + yield { [props]: result } as RelationshipsResult; + } + } + + async saveEntity( + targetInstance: E, + relationships?: Relationships + ): Promise { + if (relationships) { + for await (const item of this.asyncIterateFindRelationships( + relationships + )) { + Object.assign(targetInstance, item); + } + } + + await this.entityManager.persistAndFlush(targetInstance); + + return targetInstance; + } + + async validateRelationInputData< + Rel extends EntityRelation, + In extends InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends null | InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise>; + async validateRelationInputData< + Rel extends EntityRelation, + In extends null | InputValidateData | InputValidateData[] + >(rel: Rel, inputData: In): Promise> { + const property = Reflect.get(this.metadata.properties, rel); + const isArray = Array.isArray(inputData); + + if ( + [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes( + property.kind + ) && + !isArray + ) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data'], + message: 'Body data should be array', + }; + + throw new UnprocessableEntityException([error]); + } + + if ( + [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes( + property.kind + ) && + isArray + ) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + path: ['data'], + message: 'Body data should be object', + }; + + throw new UnprocessableEntityException([error]); + } + + if (inputData === null) { + const result = null; + return result as ValidateReturn; + } + if (isArray && inputData.length === 0) { + const result: any[] = []; + return result as ValidateReturn; + } + + const prepareData = isArray ? inputData : [inputData]; + + const errors: ValidateQueryError[] = []; + let i = 0; + const relationEntity = this.getRelation(rel as any).entity(); + const typeName = camelToKebab( + this.entityManager.getMetadata().get(relationEntity).className + ); + + for (const prepareItem of prepareData) { + if (prepareItem.type !== typeName) { + const path = isArray ? ['data', i.toString()] : ['data']; + errors.push({ + code: 'invalid_arguments', + path: path, + message: `Type should be equal to type of relName: "${rel.toString()}". Type of ${rel.toString()} is "${typeName}" but receive - "${ + prepareItem.type + }"`, + }); + } + i++; + } + if (errors.length) { + throw new UnprocessableEntityException(errors); + } + + const checkResult = await this.queryBuilder(relationEntity as any) + .where({ + [this.getPrimaryNameFor(rel)]: { + $in: prepareData.map((i) => i.id), + }, + }) + .getResult(); + + if (checkResult.length === prepareData.length) { + return ( + isArray ? inputData.map((i) => i.id) : inputData.id + ) as ValidateReturn; + } + + const resulDataMap = checkResult.reduce((acum, item) => { + acum[item[this.getPrimaryNameFor(rel)]] = true; + return acum; + }, {} as Record); + + i = 0; + for (const item of prepareData) { + if (!resulDataMap[item.id]) { + const path = isArray ? ['data', i.toString(), 'id'] : ['data', 'id']; + errors.push({ + code: 'invalid_arguments', + path: path, + message: `Not exist item "${ + item.id + }" in relation "${rel.toString()}"`, + }); + } + i++; + } + throw new NotFoundException(errors); + } +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts new file mode 100644 index 00000000..089ad7c5 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts @@ -0,0 +1,674 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ApplicationConfig } from '@nestjs/core'; + +import { JsonApiTransformerService } from './json-api-transformer.service'; +import { + Addresses, + Roles, + Users, + Notes, + Comments, + UserGroups, + dbRandomName, + mockDbPgLiteTestModule, +} from '../../../mock-utils/microrom'; + +import { faker } from '@faker-js/faker'; +import { Collection, MikroORM } from '@mikro-orm/core'; +import { + CurrentEntityManager, + CurrentEntityMetadata, + CurrentMicroOrmProvider, + EntityPropsMap, +} from '../../micro-orm/factory'; +import { + CURRENT_ENTITY, + ENTITY_MAP_PROPS, + GLOBAL_MODULE_OPTIONS_TOKEN, +} from '../../../constants'; +import { DEFAULT_ARRAY_TYPE } from '../../micro-orm/constants'; +import { EntityClass } from '../../../types'; +import { ZodEntityProps } from '../types'; + +describe('JsonApiTransformerService - extractAttributes', () => { + let service: JsonApiTransformerService; + const userObject: Users = {} as Users; + const urlPrefix = 'api'; + const version = '1'; + let mikroORM: MikroORM; + let dbName: string; + let mapProps: Map, ZodEntityProps>; + let mapPropsUser: ZodEntityProps; + beforeAll(async () => { + dbName = dbRandomName(true); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDbPgLiteTestModule(dbName)], + providers: [ + CurrentMicroOrmProvider(), + CurrentEntityManager(), + CurrentEntityMetadata(), + JsonApiTransformerService, + { + provide: ApplicationConfig, + useValue: { + getGlobalPrefix: () => urlPrefix, + getVersioning: () => ({ + defaultVersion: version, + type: 0, + }), + }, + }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap([ + Addresses, + Roles, + Users, + Notes, + Comments, + UserGroups, + ] as any), + { + provide: GLOBAL_MODULE_OPTIONS_TOKEN, + useValue: { options: { arrayType: DEFAULT_ARRAY_TYPE } }, + }, + ], + }).compile(); + + service = module.get>( + JsonApiTransformerService + ); + mapProps = module.get(ENTITY_MAP_PROPS); + const mapPropsUserCheck = mapProps.get(Users); + if (!mapPropsUserCheck) throw new Error('Not found map property for Users'); + mapPropsUser = mapPropsUserCheck; + mikroORM = module.get(MikroORM); + }); + + afterAll(() => { + mikroORM.close(true); + }); + + beforeEach(async () => { + userObject.id = faker.number.int(); + userObject.firstName = faker.person.firstName(); + userObject.lastName = faker.person.lastName(); + userObject.isActive = faker.datatype.boolean(); + userObject.login = faker.internet.userName({ + lastName: userObject.lastName, + firstName: userObject.firstName, + }); + userObject.testReal = [faker.number.float({ fractionDigits: 4 })]; + userObject.testArrayNull = null; + + userObject.testDate = faker.date.anytime(); + }); + + describe('extractAttributes', () => { + it('should extract specified fields from an object', () => { + const fields: (keyof Users)[] = ['firstName', 'lastName', 'login']; + const result = service.extractAttributes(userObject, fields); + + expect(result).toEqual({ + firstName: userObject.firstName, + lastName: userObject.lastName, + login: userObject.login, + }); + }); + + it('should return an empty object if no fields match', () => { + const fields = ['nonExistentField'] as any; + const result = service.extractAttributes(userObject, fields); + + expect(result).toEqual({}); + }); + + it('should handle an empty fields array', () => { + const fields: any[] = []; + const result = service.extractAttributes(userObject, fields); + + expect(result).toEqual({}); + }); + + it('should handle an empty input object', () => { + const inputItem = {}; + const fields: any = ['name', 'description']; + const result = service.extractAttributes(inputItem, fields); + + expect(result).toEqual({}); + }); + + it('should not include fields not present in input object', () => { + const fields = ['firstName', 'lastName', 'login', 'description'] as any; + const result = service.extractAttributes(userObject, fields); + + expect(result).toEqual({ + firstName: userObject.firstName, + lastName: userObject.lastName, + login: userObject.login, + }); + }); + }); + describe('transformRelationships', () => { + beforeEach(() => { + userObject.addresses = { + id: faker.number.int(), + city: faker.location.city(), + country: faker.location.country(), + arrayField: [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ], + state: faker.location.state(), + } as Addresses; + userObject.roles = [ + { + id: faker.number.int(), + key: faker.string.alphanumeric(5), + name: faker.word.words(), + }, + ] as unknown as Collection; + + userObject.manager = null as any; + userObject.notes = [] as any; + }); + + it('should transform relationships without "include" option enabled', () => { + const query = { include: [] } as any; + + const result = service.transformRelationships( + userObject, + mapPropsUser, + query + ); + const checkData = mapPropsUser.relations.reduce((acum, i) => { + acum[i] = { + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}/relationships/${i}`, + }, + }; + return acum; + }, {} as Record); + expect(result).toEqual(checkData); + }); + + it('should transform relationships with "include" option enabled', () => { + const query = { include: ['roles'] } as any; + + const result = service.transformRelationships( + userObject, + mapPropsUser, + query + ); + + const checkData = mapPropsUser.relations.reduce((acum, i) => { + acum[i] = { + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}/relationships/${i}`, + }, + }; + if (i === 'roles') { + acum[i]['data'] = userObject.roles.map((relName) => ({ + id: relName.id, + type: mapProps.get(Roles)?.typeName, + })); + } + return acum; + }, {} as Record); + expect(result).toEqual(checkData); + + query.include = ['addresses']; + const result1 = service.transformRelationships( + userObject, + mapPropsUser, + query + ); + const checkData1 = mapPropsUser.relations.reduce((acum, i) => { + acum[i] = { + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}/relationships/${i}`, + }, + }; + if (i === 'addresses') { + acum[i]['data'] = { + id: userObject.addresses.id, + type: mapProps.get(Addresses)?.typeName, + }; + } + return acum; + }, {} as Record); + expect(result1).toEqual(checkData1); + + query.include = ['manager', 'notes']; + const result2 = service.transformRelationships( + userObject, + mapPropsUser, + query + ); + const checkData2 = mapPropsUser.relations.reduce((acum, i) => { + acum[i] = { + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}/relationships/${i}`, + }, + }; + if (i === 'manager') { + acum[i]['data'] = null; + } + if (i === 'notes') { + acum[i]['data'] = []; + } + return acum; + }, {} as Record); + + expect(result2).toEqual(checkData2); + }); + + it('should return an empty object for empty relationships array', () => { + const query = { include: [] } as any; + + const result = service.transformRelationships( + userObject, + { ...mapPropsUser, relations: [] as any }, + query + ); + + expect(result).toEqual({}); + }); + }); + describe('extractIncluded', () => { + const roleFake = {} as Roles; + beforeEach(() => { + userObject.addresses = { + id: faker.number.int(), + city: faker.location.city(), + country: faker.location.country(), + arrayField: [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ], + state: faker.location.state(), + } as Addresses; + roleFake.id = faker.number.int(); + roleFake.key = faker.string.alphanumeric(5); + roleFake.name = faker.word.words(); + + userObject.roles = [roleFake] as unknown as Collection; + + userObject.manager = null as any; + userObject.notes = [] as any; + }); + + it('should by include', () => { + const query = { + include: ['roles', 'addresses', 'manager', 'notes'], + } as any; + + const result = service.extractIncluded([userObject], query); + const rolesInclude = result.find( + (i) => i.type === mapProps.get(Roles)?.typeName + ); + const addressesInclude = result.find( + (i) => i.type === mapProps.get(Addresses)?.typeName + ); + const managerInclude = result.find( + (i) => i.type === mapProps.get(Users)?.typeName + ); + const notesInclude = result.find( + (i) => i.type === mapProps.get(Notes)?.typeName + ); + + expect(notesInclude).toBe(undefined); + expect(managerInclude).toBe(undefined); + + const { id: roleId, ...checkRolesAttr } = roleFake; + expect(rolesInclude).toEqual({ + id: roleId.toString(), + type: mapProps.get(Roles)?.typeName, + attributes: checkRolesAttr, + links: { + self: `/api/v1/${mapProps.get(Roles)?.typeName}/${roleId}`, + }, + relationships: { + users: { + links: { + self: `/api/v${version}/${ + mapProps.get(Roles)?.typeName + }/${roleId}/relationships/users`, + }, + }, + }, + }); + const { id: addressesId, ...addressesAttr } = userObject.addresses; + expect(addressesInclude).toEqual({ + id: addressesId.toString(), + type: mapProps.get(Addresses)?.typeName, + attributes: addressesAttr, + links: { + self: `/api/v1/${mapProps.get(Addresses)?.typeName}/${addressesId}`, + }, + relationships: { + user: { + links: { + self: `/api/v${version}/${ + mapProps.get(Addresses)?.typeName + }/${addressesId}/relationships/user`, + }, + }, + }, + }); + }); + + it('should ne include by custom select', () => { + const userObject = { + id: faker.number.int(), + firstName: faker.person.firstName(), + isActive: faker.datatype.boolean(), + } as any; + userObject.addresses = { + id: faker.number.int(), + city: faker.location.city(), + country: faker.location.country(), + arrayField: [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ], + state: faker.location.state(), + } as Addresses; + userObject.comments = [ + { + id: faker.number.int(), + text: faker.lorem.text(), + kind: undefined, + createdAt: undefined, + updatedAt: undefined, + createdBy: undefined, + }, + ] as any; + + userObject.manager = { + id: 1, + login: faker.internet.userName({ + lastName: faker.person.lastName(), + firstName: faker.person.firstName(), + }), + firstName: undefined, + testReal: undefined, + testArrayNull: undefined, + lastName: undefined, + isActive: undefined, + testDate: undefined, + createdAt: undefined, + updatedAt: undefined, + addresses: undefined, + manager: undefined, + userGroup: undefined, + }; + + const query = { + include: ['addresses', 'comments', 'manager'], + fields: { + target: ['firstName', 'isActive'], + comments: ['text'], + manager: ['login'], + }, + } as any; + + const result = service.extractIncluded([userObject], query); + + const rolesInclude = result.find( + (i) => i.type === mapProps.get(Roles)?.typeName + ); + const addressesInclude = result.find( + (i) => i.type === mapProps.get(Addresses)?.typeName + ); + const managerInclude = result.find( + (i) => i.type === mapProps.get(Users)?.typeName + ); + const commentsInclude = result.find( + (i) => i.type === mapProps.get(Comments)?.typeName + ); + const notesInclude = result.find( + (i) => i.type === mapProps.get(Notes)?.typeName + ); + + expect(notesInclude).toBe(undefined); + expect(rolesInclude).toBe(undefined); + + const { id: addressesId, ...addressesAttr } = userObject.addresses; + expect(addressesInclude).toEqual({ + id: addressesId.toString(), + type: mapProps.get(Addresses)?.typeName, + attributes: addressesAttr, + links: { + self: `/api/v1/${mapProps.get(Addresses)?.typeName}/${addressesId}`, + }, + relationships: { + user: { + links: { + self: `/api/v${version}/${ + mapProps.get(Addresses)?.typeName + }/${addressesId}/relationships/user`, + }, + }, + }, + }); + + const { id: commentsId } = userObject.comments[0]; + expect(commentsInclude).toEqual({ + id: commentsId.toString(), + type: mapProps.get(Comments)?.typeName, + attributes: query.fields.comments.reduce((acum: any, field: any) => { + acum[field] = userObject.comments[0][field]; + return acum; + }, {}), + links: { + self: `/api/v1/${mapProps.get(Comments)?.typeName}/${commentsId}`, + }, + relationships: { + createdBy: { + links: { + self: `/api/v1/${ + mapProps.get(Comments)?.typeName + }/${commentsId}/relationships/createdBy`, + }, + }, + }, + }); + + const { id: managerId } = userObject.manager; + + expect(managerInclude).toEqual({ + id: managerId.toString(), + type: mapProps.get(Users)?.typeName, + attributes: query.fields.manager.reduce((acum: any, field: any) => { + acum[field] = userObject.manager[field]; + return acum; + }, {}), + links: { + self: `/api/v1/${mapProps.get(Users)?.typeName}/${managerId}`, + }, + relationships: { + addresses: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/addresses`, + }, + }, + manager: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/manager`, + }, + }, + roles: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/roles`, + }, + }, + userGroup: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/userGroup`, + }, + }, + comments: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/comments`, + }, + }, + notes: { + links: { + self: `/api/v1/${ + mapProps.get(Users)?.typeName + }/${managerId}/relationships/notes`, + }, + }, + }, + }); + }); + }); + describe('transformItem', () => { + it('should transform a single item', () => { + const query = { include: [] } as any; + const result = service.transformItem(userObject, mapPropsUser, query); + + const { id, manager, notes, roles, addresses, ...checkAttr } = userObject; + + expect(result).toEqual({ + id: userObject.id.toString(), + type: mapPropsUser.typeName, + attributes: checkAttr, + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}`, + }, + relationships: { + addresses: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/addresses`, + }, + }, + manager: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/manager`, + }, + }, + roles: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/roles`, + }, + }, + userGroup: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/userGroup`, + }, + }, + comments: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/comments`, + }, + }, + notes: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/notes`, + }, + }, + }, + }); + }); + + it('should be transform with custom select field and include is null', () => { + const userObject = { + id: faker.number.int(), + firstName: faker.person.firstName(), + isActive: faker.datatype.boolean(), + } as any; + userObject.addresses = { + id: faker.number.int(), + city: faker.location.city(), + country: faker.location.country(), + arrayField: [ + faker.string.alphanumeric(5), + faker.string.alphanumeric(5), + ], + state: faker.location.state(), + } as Addresses; + userObject.comments = [] as any; + userObject.manager = null as any; + Object.assign(userObject, { + Comments__comments__id: null, + Comments__comments__text: null, + Users__manager__id: null, + Users__manager__login: null, + }); + + const query = { + include: ['addresses', 'comments', 'manager'], + fields: { + target: ['firstName', 'isActive'], + comments: ['text'], + manager: ['login'], + }, + } as any; + + const result = service.transformItem(userObject, mapPropsUser, query); + expect(result).toEqual({ + id: userObject.id.toString(), + type: mapPropsUser.typeName, + attributes: query.fields.target.reduce((acum: any, i: any) => { + acum[i] = userObject[i]; + return acum; + }, {} as any), + links: { + self: `/${urlPrefix}/v${version}/${mapPropsUser.typeName}/${userObject.id}`, + }, + relationships: { + addresses: { + data: { + id: userObject.addresses.id, + type: mapProps.get(Addresses)?.typeName, + }, + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/addresses`, + }, + }, + manager: { + data: null, + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/manager`, + }, + }, + comments: { + data: [], + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/comments`, + }, + }, + roles: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/roles`, + }, + }, + userGroup: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/userGroup`, + }, + }, + notes: { + links: { + self: `/api/v1/${mapPropsUser.typeName}/${userObject.id}/relationships/notes`, + }, + }, + }, + }); + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts new file mode 100644 index 00000000..6cff4a9b --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts @@ -0,0 +1,322 @@ +import { Inject, Injectable, VersioningType } from '@nestjs/common'; +import { ApplicationConfig } from '@nestjs/core'; +import { + Attributes, + camelToKebab, + Data, + DataResult, + EntityRelation, + Include, + MainData, + ObjectTyped, + Relationships, + ResourceData, + ResourceObject, +} from '@klerick/json-api-nestjs-shared'; + +import { EntityClass, ObjectLiteral } from '../../../types'; +import { ENTITY_MAP_PROPS, CURRENT_ENTITY } from '../../../constants'; +import { + EntityProps, + RelationProperty, + ZodEntityProps, + ZodParams, +} from '../types'; +import { Query, QueryOne } from '../zod'; +import { RoutePathFactory } from '@nestjs/core/router/route-path-factory'; + +Injectable(); +export class JsonApiTransformerService { + @Inject(ApplicationConfig) private applicationConfig!: ApplicationConfig; + @Inject(ENTITY_MAP_PROPS) private entityMapProps!: Map< + EntityClass, + ZodEntityProps + >; + @Inject(CURRENT_ENTITY) private currentEntity!: EntityClass; + + private _urlPath!: string[]; + private _currentMapProps!: ZodEntityProps; + + get currentMapProps(): ZodEntityProps { + if (!this._currentMapProps) { + const result = this.entityMapProps.get(this.currentEntity); + if (!result) + throw new Error('Not found map for ' + this.currentEntity.name); + + this._currentMapProps = result; + } + + return this._currentMapProps; + } + + get urlPath() { + if (this._urlPath) return [...this._urlPath]; + this._urlPath = ['']; + const prefix = this.applicationConfig.getGlobalPrefix(); + const version = this.applicationConfig.getVersioning(); + + const routePathFactory = new RoutePathFactory(this.applicationConfig); + + if (prefix) { + this._urlPath.push(this.applicationConfig.getGlobalPrefix()); + } + if (version && version.type === VersioningType.URI) { + const firstVersion = Array.isArray(version.defaultVersion) + ? version.defaultVersion[0] + : version.defaultVersion; + if (firstVersion) { + this._urlPath.push( + `${routePathFactory.getVersionPrefix( + version + )}${firstVersion.toString()}` + ); + } + } + return [...this._urlPath]; + } + + public transformData( + data: E, + query: QueryOne + ): Pick, 'data' | 'included'>; + public transformData( + data: E[], + query: Query + ): Pick, 'data' | 'included'>; + public transformData( + data: E | E[], + query: Query + ): + | Pick, 'data' | 'included'> + | Pick, 'data' | 'included'> { + if (Array.isArray(data)) { + const resultData: Pick< + ResourceObject, + 'data' | 'included' + > = { + data: data.map((item) => + this.transformItem(item, this.currentMapProps, query) + ), + }; + + if (query.include) { + resultData.included = this.extractIncluded(data, query); + } + + return resultData; + } + + const resultData: Pick, 'data' | 'included'> = { + data: this.transformItem(data, this.currentMapProps, query), + }; + + if (query.include) { + resultData.included = this.extractIncluded([data], query); + } + + return resultData; + } + + public transformItem( + item: T, + mapProps: ZodEntityProps, + query: Query + ): ResourceData { + const { fields } = query; + const target = Reflect.get(fields || {}, 'target'); + return { + id: item[mapProps.primaryColumnName].toString(), + type: mapProps.typeName, + attributes: this.extractAttributes( + item, + mapProps.props.filter((i) => { + if (i === mapProps.primaryColumnName) { + return false; + } + if (!target) { + return true; + } + + return (target as string[]).includes(i); + }) + ), + links: { + self: this.getLink(mapProps.typeName, item[mapProps.primaryColumnName]), + }, + relationships: this.transformRelationships(item, mapProps, query), + }; + } + + public transformRel>( + item: E, + rel: Rel + ): DataResult { + const relProps = Reflect.get(this.currentMapProps.relationProperty, rel); + const relationMapPops = this.entityMapProps.get(relProps.entityClass); + if (!relationMapPops) + throw new Error('Not found props map for ' + relProps.entityClass); + const props = item[rel]; + + if (Array.isArray(props)) { + return props.map((i: any) => ({ + type: relationMapPops.typeName, + id: i[relationMapPops.primaryColumnName], + })); + } else { + return { + type: relationMapPops.typeName, + id: props.primaryColumnName, + } as any; + } + } + + public transformRelationships( + item: T, + mapProps: ZodEntityProps, + query: Query + ): Relationships { + const { include } = query; + + const includeMap = new Map((include || []).map((i) => [i, true])); + + return mapProps.relations.reduce((acum, i: keyof RelationProperty) => { + acum[i as keyof Relationships] = { + links: { + self: this.getLink( + mapProps.typeName, + item[mapProps.primaryColumnName], + 'relationships', + i + ), + }, + }; + + if (includeMap.has(i)) { + const relationMapPops = this.entityMapProps.get( + mapProps.relationProperty[i].entityClass + ); + if (!relationMapPops) + throw new Error( + 'Not found props map for ' + + mapProps.relationProperty[i].entityClass.name + ); + if (mapProps.relationProperty[i].isArray) { + if (item[i] && Array.isArray(item[i]) && item[i].length > 0) { + // @ts-expect-error incorrect parse + acum[i as keyof Relationships]['data'] = item[i].map( + (rel: any) => ({ + id: rel[relationMapPops.primaryColumnName], + type: relationMapPops.typeName, + }) + ); + } else { + // @ts-expect-error incorrect parse + acum[i as keyof Relationships]['data'] = []; + } + } else { + // @ts-expect-error incorrect parse + acum[i as keyof Relationships]['data'] = item[i] + ? { + id: item[i][relationMapPops.primaryColumnName], + type: relationMapPops.typeName, + } + : null; + } + } + + return acum; + }, {} as Relationships); + } + + public extractAttributes( + item: T, + fields: (keyof T)[] + ): Attributes { + const mapFields = fields.reduce((acum, field) => { + acum[field.toString()] = true; + return acum; + }, {} as Record); + return ObjectTyped.entries(item).reduce((acum, [name, value]) => { + if (name in mapFields && mapFields[name.toString()]) { + // @ts-expect-error assign key to object entity + acum[name] = value; + } + return acum; + }, {} as Attributes); + } + + public extractIncluded( + data: T[], + query: Query + ): Include[] { + const includeArray: any[] = []; + const { include } = query; + if (!include) return []; + for (const relationPropsFromInclude of include) { + const relationProps = + this.currentMapProps.relationProperty[relationPropsFromInclude]; + if (!relationProps) continue; + const relationMapProp = this.entityMapProps.get( + relationProps.entityClass + ); + if (!relationMapProp) + throw new Error( + 'Not found props for relation ' + + relationPropsFromInclude + + 'in' + + this.currentEntity.name + ); + const { fields } = query; + + const selectFieldForInclude = Reflect.get( + fields || {}, + relationPropsFromInclude + ); + + const queryForInclude = { + ...query, + fields: { + target: + selectFieldForInclude && + Array.isArray(selectFieldForInclude) && + selectFieldForInclude.length > 0 + ? selectFieldForInclude + : null, + }, + include: null, + }; + + for (const dataItem of data) { + const propRel = dataItem[relationPropsFromInclude]; + if (!propRel) continue; + if (Array.isArray(propRel)) { + for (const i of propRel as any) { + includeArray.push( + this.transformItem( + i, + relationMapProp as unknown as ZodEntityProps, + queryForInclude as Query + ) + ); + } + } else { + includeArray.push( + this.transformItem( + propRel, + relationMapProp as unknown as ZodEntityProps, + queryForInclude as Query + ) + ); + } + } + } + + return includeArray; + } + + private getLink(...partOfUrl: string[]) { + const urlPath = this.urlPath; + urlPath.push(...partOfUrl); + return urlPath.join('/'); + } +} From 4696f51b7d075f4ed15a1ae2d72a8de00324ba91 Mon Sep 17 00:00:00 2001 From: Alex H Date: Mon, 10 Feb 2025 10:45:53 +0100 Subject: [PATCH 16/26] feat(json-api-nestjs): Microro orm Create orm methode, create general transform service, use this service for swagger and zod --- .../app/resources/controllers/entity-orm.ts | 2 +- .../extend-book-list.controller.ts | 2 +- .../src/lib/types/response-body.ts | 9 +- .../src/lib/types/utils-string.type.ts | 8 +- .../json-api-nestjs/src/lib/constants/di.ts | 2 + .../src/lib/mock-utils/index.ts | 27 ++ .../lib/mock-utils/microrom/entities/notes.ts | 2 +- .../src/lib/mock-utils/microrom/index.ts | 117 ++++++- .../lib/mock-utils/microrom/utils/index.ts | 1 + .../mock-utils/microrom/utils/pull-data.ts | 3 +- .../factory/zod-input-operation.ts | 12 +- .../utils/zod/zod-helper.spec.ts | 41 ++- .../atomic-operation/utils/zod/zod-helper.ts | 15 +- .../lib/modules/micro-orm/factory/index.ts | 120 ++----- .../micro-orm/micro-orm-json-api.module.ts | 10 +- .../micro-orm/orm-helper/index.spec.ts | 330 +++++++----------- .../lib/modules/micro-orm/orm-helper/index.ts | 196 +++-------- .../micro-orm/service/microorm-service.ts | 186 ++++++++-- .../mixin/factory/zod-validate.factory.ts | 218 +++++++++--- .../src/lib/modules/mixin/mixin.module.ts | 10 +- .../modules/mixin/pipe/query/query.pipe.ts | 1 + .../mixin/swagger/method/delete-one.ts | 13 +- .../swagger/method/delete-relationship.ts | 18 +- .../modules/mixin/swagger/method/get-all.ts | 39 ++- .../modules/mixin/swagger/method/get-one.ts | 41 ++- .../mixin/swagger/method/get-relationship.ts | 20 +- .../modules/mixin/swagger/method/patch-one.ts | 24 +- .../swagger/method/patch-relationship.ts | 17 +- .../modules/mixin/swagger/method/post-one.ts | 22 +- .../mixin/swagger/method/post-relationship.ts | 17 +- .../mixin/swagger/swagger-bind.service.ts | 25 +- .../src/lib/modules/mixin/swagger/utils.ts | 104 +++--- .../src/lib/modules/mixin/types/zod-types.ts | 38 +- .../src/lib/modules/mixin/zod/index.ts | 2 + .../zod/zod-input-query-schema/filter.spec.ts | 4 +- .../mixin/zod/zod-share/relationships.ts | 4 + .../lib/modules/type-orm/orm-methods/index.ts | 12 - .../service/typeorm-utils.service.spec.ts | 23 +- .../type-orm/service/typeorm-utils.service.ts | 6 +- .../src/lib/modules/type-orm/type.ts | 29 +- .../json-api-nestjs/src/lib/types/index.ts | 1 - .../json-api-nestjs/src/lib/types/operand.ts | 26 -- libs/microorm-database/src/lib/config-cli.ts | 4 +- .../src/lib/entities/users.ts | 4 +- package-lock.json | 31 +- package.json | 3 + 46 files changed, 1058 insertions(+), 781 deletions(-) delete mode 100644 libs/json-api/json-api-nestjs/src/lib/types/operand.ts diff --git a/apps/json-api-server/src/app/resources/controllers/entity-orm.ts b/apps/json-api-server/src/app/resources/controllers/entity-orm.ts index 97042f97..a95b88e7 100644 --- a/apps/json-api-server/src/app/resources/controllers/entity-orm.ts +++ b/apps/json-api-server/src/app/resources/controllers/entity-orm.ts @@ -5,6 +5,6 @@ import { BookList as tBookList } from '@nestjs-json-api/typeorm-database'; import { BookList as mkBookList } from '@nestjs-json-api/microorm-database'; const Users = process.env['ORM_TYPE'] === 'typeorm' ? tUsers : mkUsers; -const BookList = process.env['ORM_TYPE'] === 'typeorm' ? tBookList : tBookList; +const BookList = process.env['ORM_TYPE'] === 'typeorm' ? tBookList : mkBookList; export { Users, BookList }; diff --git a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts b/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts index 91b59a04..248f78b0 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts +++ b/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts @@ -2,7 +2,7 @@ import { ParseUUIDPipe } from '@nestjs/common'; import { BookList } from '../entity-orm'; import { JsonApi, JsonBaseController } from '@klerick/json-api-nestjs'; -@JsonApi(BookList as typeof BookList, { +@JsonApi(BookList as any, { pipeForId: ParseUUIDPipe, overrideRoute: 'override-book-list', allowMethod: ['getOne', 'postOne', 'deleteOne'], diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts index fbe91dde..85dbdb49 100644 --- a/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/response-body.ts @@ -5,6 +5,7 @@ import { TypeOfArray, ValueOf, } from '.'; +import { Collection } from '@mikro-orm/core'; export type PageProps = { totalItems: number; @@ -30,8 +31,14 @@ export type Attributes = { [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; }; +export type DataResult = E extends unknown[] + ? MainData[] + : E extends Collection + ? MainData[] + : MainData | null; + export type Data = { - data?: E extends unknown[] ? MainData[] : MainData | null; + data?: DataResult; }; export type Relationships = { diff --git a/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts b/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts index 2d4284b2..b861d238 100644 --- a/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts +++ b/libs/json-api/json-api-nestjs-shared/src/lib/types/utils-string.type.ts @@ -1,3 +1,5 @@ +import type { Collection } from '@mikro-orm/core'; + export type KebabCase = S extends `${infer C}${infer T}` ? KebabCase extends infer U ? U extends string @@ -15,6 +17,10 @@ export type KebabToCamelCase = ? `${Capitalize}${Capitalize>}` : S; -export type TypeOfArray = T extends (infer U)[] ? U : T; +export type TypeOfArray = T extends (infer U)[] + ? U + : T extends Collection + ? U + : T; export type ValueOf = T[keyof T]; diff --git a/libs/json-api/json-api-nestjs/src/lib/constants/di.ts b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts index ed2d39a1..2d70e17d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/constants/di.ts +++ b/libs/json-api/json-api-nestjs/src/lib/constants/di.ts @@ -28,3 +28,5 @@ export const ZOD_PATCH_RELATIONSHIP_SCHEMA = Symbol( ); export const CURRENT_DATA_SOURCE_TOKEN = Symbol('CURRENT_DATA_SOURCE_TOKEN'); export const CURRENT_ENTITY_REPOSITORY = Symbol('CURRENT_ENTITY_REPOSITORY'); + +export const ENTITY_MAP_PROPS = Symbol('ENTITY_MAP_PROPS'); diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts index 55385b02..b9efca01 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/index.ts @@ -2,6 +2,33 @@ import { DataType, IMemoryDb, newDb } from 'pg-mem'; import { readFileSync } from 'fs'; import { join } from 'path'; import { v4 } from 'uuid'; +// @ts-ignore +import type { PGlite } from '@electric-sql/pglite'; + +export async function createAndPullSchemaBasePgLite(): Promise { + const db = await Promise.all([ + import('@electric-sql/pglite'), + // @ts-ignore + import('@electric-sql/pglite/contrib/uuid_ossp'), + ]).then( + ([{ PGlite }, { uuid_ossp }]) => + new PGlite({ + extensions: { uuid_ossp }, + database: 'pgLite', + username: 'postgres', + }) + ); + + // await db.exec( + // 'CREATE SCHEMA IF NOT EXISTS public; SET search_path TO public;' + // ); + + // const dump = readFileSync(join(__dirname, 'db-for-test'), { + // encoding: 'utf8', + // }); + // await db.exec(dump); + return db; +} export function createAndPullSchemaBase(): IMemoryDb { const dump = readFileSync(join(__dirname, 'db-for-test'), { diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts index f79aa30e..b3ff70ce 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/notes.ts @@ -8,7 +8,7 @@ import { Users, IUsers } from './index'; export class Notes { @PrimaryKey({ type: 'uuid', - defaultRaw: 'uuid_generate_v4()', + defaultRaw: 'gen_random_uuid()', }) public id!: string; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts index f7d5aa29..c481f17a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts @@ -1,7 +1,10 @@ import { DynamicModule } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; import { MikroOrmModule } from '@mikro-orm/nestjs'; -import { MikroORM } from '@mikro-orm/core'; +import { EntityManager, MikroORM } from '@mikro-orm/core'; import { PostgreSqlDriver } from '@mikro-orm/postgresql'; +import { SqlHighlighter } from '@mikro-orm/sql-highlighter'; +import { QueryField } from '@klerick/json-api-nestjs-shared'; import { IMemoryDb } from 'pg-mem'; @@ -13,20 +16,54 @@ import { UserGroups, Users, } from './entities'; +import { ObjectLiteral } from '../../types'; +import { Query } from '../../modules/mixin/zod'; + +import { + CurrentEntityManager, + CurrentEntityMetadata, + CurrentEntityRepository, + CurrentMicroOrmProvider, + OrmServiceFactory, + EntityPropsMap, +} from '../../modules/micro-orm/factory'; +import { MicroOrmUtilService } from '../../modules/micro-orm/service/micro-orm-util.service'; +import { CURRENT_ENTITY, GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; export * from './entities'; export * from './utils'; export const entities = [Users, UserGroups, Roles, Comments, Addresses, Notes]; +import { sharedConnect, initMikroOrm, pullAllData } from './utils'; +import { DEFAULT_ARRAY_TYPE } from '../../modules/micro-orm/constants'; +import { JsonApiTransformerService } from '../../modules/mixin/service/json-api-transformer.service'; + +export function mockDbPgLiteTestModule(dbName = `test_db_${Date.now()}`) { + const mikroORM = { + provide: MikroORM, + useFactory: async () => { + const knexInst = await sharedConnect(); + return initMikroOrm(knexInst, dbName); + }, + }; + return { + module: MikroOrmModule, + providers: [mikroORM], + exports: [mikroORM], + }; +} + export function mockDBTestModule(db: IMemoryDb): DynamicModule { const mikroORM = { provide: MikroORM, useFactory: () => db.adapters.createMikroOrm({ + highlighter: new SqlHighlighter(), entities: [Users, UserGroups, Roles, Comments, Addresses, Notes], driver: PostgreSqlDriver, allowGlobalContext: true, + debug: ['query', 'query-params'], }), }; return { @@ -35,3 +72,81 @@ export function mockDBTestModule(db: IMemoryDb): DynamicModule { exports: [mikroORM], }; } +const readOnlyDbName = `readonly_db_${Date.now()}`; + +export function dbRandomName(readOnly = false) { + if (readOnly) { + return readOnlyDbName; + } + return `test_db_${Date.now()}`; +} + +export async function pullData(em: EntityManager, count = 1) { + for (let i = 0; i < count; i++) { + await pullAllData(em); + } +} + +export function getModuleForPgLite( + entity: E, + dbName = `test_db_${Date.now()}` +): Promise { + return Test.createTestingModule({ + imports: [mockDbPgLiteTestModule(dbName)], + providers: [ + CurrentMicroOrmProvider(), + CurrentEntityManager(), + CurrentEntityMetadata(), + CurrentEntityRepository(entity), + MicroOrmUtilService, + { + provide: CURRENT_ENTITY, + useValue: entity, + }, + OrmServiceFactory(), + EntityPropsMap(entities as any), + { + provide: GLOBAL_MODULE_OPTIONS_TOKEN, + useValue: { options: { arrayType: DEFAULT_ARRAY_TYPE } }, + }, + JsonApiTransformerService, + ], + }).compile(); +} + +export function getModuleFor( + db: IMemoryDb, + entity: E +): Promise { + return Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [ + CurrentMicroOrmProvider(), + CurrentEntityManager(), + CurrentEntityMetadata(), + CurrentEntityRepository(entity), + MicroOrmUtilService, + { + provide: CURRENT_ENTITY, + useValue: entity, + }, + OrmServiceFactory(), + ], + }).compile(); +} + +export function getDefaultQuery(): Query { + return { + [QueryField.filter]: { + relation: null, + target: null, + }, + [QueryField.fields]: null, + [QueryField.include]: null, + [QueryField.sort]: null, + [QueryField.page]: { + size: 1, + number: 1, + }, + } satisfies Query; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts index 902c50a6..fa93c59d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/index.ts @@ -1,2 +1,3 @@ export * from './provider-entities'; export * from './pull-data'; +export * from './init-db'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts index c0acd551..5ea47448 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/pull-data.ts @@ -38,7 +38,7 @@ export async function pullNote() { export async function pullRole() { const role = new Roles(); role.key = faker.string.alphanumeric(5); - role.name = faker.string.alphanumeric(5); + role.name = faker.word.words(); return role; } @@ -104,6 +104,7 @@ export async function pullAllData(em: EntityManager) { managerUser.addresses = address2; managerUser.userGroup = userGroup3; + managerUser.roles.add(role1, role2); await em.persistAndFlush([ user, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts index 620b2b52..75ddeb40 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/zod-input-operation.ts @@ -2,9 +2,9 @@ import { FactoryProvider } from '@nestjs/common'; import { MAP_CONTROLLER_ENTITY, ZOD_INPUT_OPERATION } from '../constants'; import { MapController } from '../types'; import { zodInputOperation, ZodInputOperation } from '../utils'; -import { FIELD_FOR_ENTITY } from '../../../constants'; -import { GetFieldForEntity } from '../../mixin/types'; -import { ObjectLiteral } from '../../../types'; +import { ENTITY_MAP_PROPS } from '../../../constants'; +import { ZodEntityProps } from '../../mixin/types'; +import { EntityClass, ObjectLiteral } from '../../../types'; export function ZodInputOperation(): FactoryProvider< ZodInputOperation @@ -13,10 +13,10 @@ export function ZodInputOperation(): FactoryProvider< provide: ZOD_INPUT_OPERATION, useFactory( mapController: MapController, - getField: GetFieldForEntity + entityMapProps: Map, ZodEntityProps> ) { - return zodInputOperation(mapController, getField); + return zodInputOperation(mapController, entityMapProps); }, - inject: [MAP_CONTROLLER_ENTITY, FIELD_FOR_ENTITY], + inject: [MAP_CONTROLLER_ENTITY, ENTITY_MAP_PROPS], }; } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts index 62b545d5..68a9977c 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.spec.ts @@ -14,11 +14,16 @@ import { ZodUpdate, } from './zod-helper'; import { Users } from '../../../../mock-utils/typeorm'; -import { FIELD_FOR_ENTITY } from '../../../../constants'; +import { ENTITY_MAP_PROPS, FIELD_FOR_ENTITY } from '../../../../constants'; import { JsonBaseController } from '../../../mixin/controller/json-base.controller'; import { MapController } from '../../types'; import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; -import { GetFieldForEntity, TupleOfEntityRelation } from '../../../mixin/types'; +import { + GetFieldForEntity, + TupleOfEntityRelation, + ZodEntityProps, +} from '../../../mixin/types'; +import { EntityClass } from '../../../../types'; describe('ZodHelperSpec', () => { afterEach(() => { @@ -444,26 +449,34 @@ describe('ZodHelperSpec', () => { }); }); describe('zodInputOperation', () => { - let getField: GetFieldForEntity; + let getField: Map, ZodEntityProps>; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ { - provide: FIELD_FOR_ENTITY, - useValue: () => ({ - relations: [ - 'userGroup', - 'notes', - 'comments', - 'roles', - 'manager', - 'addresses', + provide: ENTITY_MAP_PROPS, + useValue: new Map([ + [ + Users, + { + relations: [ + 'userGroup', + 'notes', + 'comments', + 'roles', + 'manager', + 'addresses', + ], + }, ], - }), + ]), }, ], }).compile(); - getField = module.get>(FIELD_FOR_ENTITY); + getField = + module.get, ZodEntityProps>>( + ENTITY_MAP_PROPS + ); }); it('should be correct', () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts index 3b2734c7..fb73822a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts @@ -13,9 +13,13 @@ import { camelToKebab } from '@klerick/json-api-nestjs-shared'; import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; import { MapController } from '../../types'; -import { GetFieldForEntity, TupleOfEntityRelation } from '../../../mixin/types'; +import { + GetFieldForEntity, + TupleOfEntityRelation, + ZodEntityProps, +} from '../../../mixin/types'; import { getEntityName } from '../../../mixin/helper'; -import { ObjectLiteral } from '../../../../types'; +import { EntityClass, ObjectLiteral } from '../../../../types'; export enum Operation { add = 'add', @@ -131,7 +135,7 @@ export type InputArray = z.infer; export function zodInputOperation( mapController: MapController, - getField: GetFieldForEntity + entityMapProps: Map, ZodEntityProps> ) { const array = [] as unknown as [ ZodAdd, @@ -143,7 +147,10 @@ export function zodInputOperation( ]; for (const [entity, controller] of mapController.entries()) { const typeName = camelToKebab(getEntityName(entity)); - const { relations } = getField(entity); + const entityMap = entityMapProps.get(entity as any); + if (!entityMap) throw new Error('Entity not found in map'); + + const { relations } = entityMap; const hasOwnProperty = (props: string) => Object.prototype.hasOwnProperty.call(controller.prototype, props); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts index 860650e2..d1e1c088 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts @@ -3,10 +3,9 @@ import { EntityManager, MikroORM, EntityRepository, - EntityMetadata, MetadataStorage, } from '@mikro-orm/core'; -import { camelToKebab, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; import { getMikroORMToken } from '@mikro-orm/nestjs'; import { @@ -15,39 +14,27 @@ import { CURRENT_ENTITY_REPOSITORY, FIELD_FOR_ENTITY, GLOBAL_MODULE_OPTIONS_TOKEN, - PARAMS_FOR_ZOD_SCHEMA, RUN_IN_TRANSACTION_FUNCTION, ORM_SERVICE, + ENTITY_MAP_PROPS, } from '../../../constants'; import { - ConfigParam, EntityClass, - EntityName, EntityTarget, ObjectLiteral, - RequiredFromPartial, - ResultGeneralParam, ResultMicroOrmModuleOptions, RunInTransaction, } from '../../../types'; +import { GetFieldForEntity, ZodEntityProps } from '../../mixin/types'; import { - EntityProps, - FieldWithType, - GetFieldForEntity, - ZodParams, -} from '../../mixin/types'; -import { - getField, - getPropsTreeForRepository, - getArrayPropsForEntity, - getTypeForAllProps, - getRelationTypeArray, - getTypePrimaryColumn, - getFieldWithType, - getPropsFromDb, - getRelationTypeName, - getRelationTypePrimaryColumn, + getProps, + getRelation, + getPropsType, + getPropsNullable, + getPrimaryColumnName, + getPrimaryColumnType, + getRelationProperty, } from '../orm-helper'; import { getEntityName } from '../../mixin/helper'; @@ -59,7 +46,9 @@ export function CurrentMicroOrmProvider( ): FactoryProvider { return { provide: CURRENT_DATA_SOURCE_TOKEN, - useFactory: (mikroORM: MikroORM) => mikroORM, + useFactory: (mikroORM: MikroORM) => { + return mikroORM; + }, inject: [connectionName ? getMikroORMToken(connectionName) : MikroORM], }; } @@ -91,75 +80,34 @@ export function CurrentEntityMetadata(): FactoryProvider { }; } -export function GetFieldForEntity(): FactoryProvider< - GetFieldForEntity -> { +export function EntityPropsMap( + entities: EntityClass[] +) { return { - provide: FIELD_FOR_ENTITY, - useFactory: (entityManager: EntityManager) => { - return (entity: EntityTarget) => - getField(entityManager.getMetadata().get(entity as EntityClass)); - }, - inject: [CURRENT_ENTITY_MANAGER_TOKEN], - }; -} - -export function ZodParamsFactory( - currentEntity: EntityClass -): FactoryProvider>> { - return { - provide: PARAMS_FOR_ZOD_SCHEMA, + provide: ENTITY_MAP_PROPS, inject: [ENTITY_METADATA_TOKEN, GLOBAL_MODULE_OPTIONS_TOKEN], useFactory: ( metadataStorage: MetadataStorage, config: ResultMicroOrmModuleOptions ) => { - const metadata = metadataStorage.get(currentEntity); + const mapProperty = new Map, ZodEntityProps>(); const arrayConfig = config.options.arrayType; - - const primaryColumns = metadata.getPrimaryProp() - .name as unknown as EntityProps; - - const fieldWithType = ObjectTyped.entries( - getFieldWithType(metadata, arrayConfig) - ) - .filter(([key]) => key !== primaryColumns) - .reduce( - (acum, [key, type]) => ({ - ...acum, - [key]: type, - }), - {} as FieldWithType - ); - - return { - entityFieldsStructure: getField(metadata), - entityRelationStructure: getPropsTreeForRepository( - metadataStorage, - currentEntity - ), - propsArray: getArrayPropsForEntity( - metadataStorage, - currentEntity, - arrayConfig - ), - propsType: getTypeForAllProps( - metadataStorage, - currentEntity, - arrayConfig - ), - typeId: getTypePrimaryColumn(metadata), - typeName: camelToKebab(getEntityName(currentEntity)), - fieldWithType, - propsDb: getPropsFromDb(metadata, arrayConfig), - primaryColumn: primaryColumns, - relationArrayProps: getRelationTypeArray(metadata), - relationPopsName: getRelationTypeName(metadata), - primaryColumnType: getRelationTypePrimaryColumn( - metadataStorage, - currentEntity - ), - } satisfies ZodParams>; + for (const item of entities) { + const metadata = metadataStorage.get(item); + const className = getEntityName(item); + mapProperty.set(item, { + props: getProps(metadata), + propsType: getPropsType(metadata, arrayConfig), + propsNullable: getPropsNullable(metadata), + primaryColumnName: getPrimaryColumnName(metadata), + primaryColumnType: getPrimaryColumnType(metadata), + typeName: camelToKebab(className), + className: className, + relations: getRelation(metadata), + relationProperty: getRelationProperty(metadata), + }); + } + return mapProperty; }, }; } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts index 5e4883c9..6e9e443f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/micro-orm-json-api.module.ts @@ -8,11 +8,11 @@ import { CurrentEntityMetadata, CurrentEntityRepository, CurrentMicroOrmProvider, - GetFieldForEntity, OrmServiceFactory, RunInTransactionFactory, - ZodParamsFactory, + EntityPropsMap, } from './factory'; +import { MicroOrmUtilService } from './service/micro-orm-util.service'; export class MicroOrmJsonApiModule { static module = 'microOrm' as const; @@ -32,8 +32,9 @@ export class MicroOrmJsonApiModule { optionProvider, CurrentMicroOrmProvider(options.connectionName), CurrentEntityManager(), - GetFieldForEntity(), + CurrentEntityMetadata(), RunInTransactionFactory(), + EntityPropsMap(options.entities), ]; const currentImport = [microOrmModule, ...(options.imports || [])]; @@ -49,9 +50,8 @@ export class MicroOrmJsonApiModule { static getUtilProviders(entity: ObjectLiteral): NestProvider { return [ CurrentEntityRepository(entity), - CurrentEntityMetadata(), - ZodParamsFactory(entity as any), OrmServiceFactory(), + MicroOrmUtilService, ]; } } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts index 9326ccef..fbb9fbdd 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.spec.ts @@ -1,26 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { - MetadataStorage, - EntityManager, - Collection, - MikroORM, -} from '@mikro-orm/core'; -import { - EntityProps, - EntityRelation, - ObjectTyped, -} from '@klerick/json-api-nestjs-shared'; -import { IMemoryDb } from 'pg-mem'; +import { MetadataStorage, MikroORM } from '@mikro-orm/core'; -import { createAndPullSchemaBase } from '../../../mock-utils'; import { - mockDBTestModule, Users, - pullAllData, - pullUser, Addresses, Notes, Roles, + dbRandomName, + mockDbPgLiteTestModule, + Comments, + UserGroups, } from '../../../mock-utils/microrom'; import { CurrentMicroOrmProvider, @@ -30,35 +19,27 @@ import { import { DEFAULT_ARRAY_TYPE, ENTITY_METADATA_TOKEN } from '../constants'; +import { TypeField } from '../../mixin/types'; + import { - getField, - getPropsTreeForRepository, - getArrayPropsForEntity, - getArrayFields, - getFieldWithType, - getTypeForAllProps, - getRelationTypeArray, - getTypePrimaryColumn, - getPropsFromDb, - getRelationTypeName, - getRelationTypePrimaryColumn, + getProps, + getPropsType, + getPropsNullable, + getPrimaryColumnName, + getPrimaryColumnType, + getRelation, + getRelationProperty, } from './'; -import { CURRENT_ENTITY_MANAGER_TOKEN } from '../../../constants'; -import { ArrayPropsForEntity, PropsArray, TypeField } from '../../mixin/types'; - -describe('microorm-orm-helper', () => { - let db: IMemoryDb; +describe('microorm-orm-helper-for-map', () => { let entityMetadataToken: MetadataStorage; - let em: EntityManager; - let user: Users; - let userWithRelation: Users; let mikroORM: MikroORM; + let dbName: string; const config = DEFAULT_ARRAY_TYPE; beforeAll(async () => { - db = createAndPullSchemaBase(); + dbName = dbRandomName(true); const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], + imports: [mockDbPgLiteTestModule(dbName)], providers: [ CurrentMicroOrmProvider(), CurrentEntityManager(), @@ -67,199 +48,130 @@ describe('microorm-orm-helper', () => { }).compile(); entityMetadataToken = module.get(ENTITY_METADATA_TOKEN); - em = module.get(CURRENT_ENTITY_MANAGER_TOKEN); - mikroORM = module.get(MikroORM); - user = await pullUser(); - const { roles, comments, notes, ...other } = user; - user = other as Users; - user.id = 1; - userWithRelation = await pullAllData(em); + mikroORM = module.get(MikroORM); }); afterAll(() => { mikroORM.close(true); }); - it('getField', async () => { - const { field, relations } = getField(entityMetadataToken.get(Users)); - - const userFieldProps = Object.getOwnPropertyNames( - user - ) as EntityProps[]; - const hasUserFieldInResultField = userFieldProps.some( - (field) => !field.includes(field) - ); - - const hasResultInUserField = field.some( - (field) => !userFieldProps.includes(field) - ); - - const userRelationProps: EntityRelation[] = ( - Object.getOwnPropertyNames(userWithRelation) as (EntityProps & - EntityRelation)[] - ) - .filter((props) => !userFieldProps.includes(props)) - .filter((i) => i !== '__helper' && i !== '__gettersDefined'); - - const hasUserRelationInResultField = userRelationProps.some( - (field) => !relations.includes(field) - ); - - const hasResultInUserRelation = relations.some( - (field) => !userRelationProps.includes(field) - ); - - expect(hasUserFieldInResultField).toEqual(false); - expect(hasResultInUserField).toEqual(false); - - expect(hasUserRelationInResultField).toEqual(false); - expect(hasResultInUserRelation).toEqual(false); - }); - - it('getPropsTreeForRepository', () => { - const relationField = getPropsTreeForRepository(entityMetadataToken, Users); - const userFieldProps = Object.getOwnPropertyNames( - user - ) as EntityProps[]; - - const userRelationProps: EntityRelation[] = ( - Object.getOwnPropertyNames(userWithRelation) as (EntityProps & - EntityRelation)[] - ) - .filter((props) => !userFieldProps.includes(props)) - .filter((i) => i !== '__helper' && i !== '__gettersDefined'); - - const hasUserRelationInResultField = userRelationProps.some( - (field) => !Object.keys(relationField).includes(field) - ); - const hasResultInUserRelation = ObjectTyped.keys(relationField).some( - (field) => !userRelationProps.includes(field) - ); - expect(hasUserRelationInResultField).toEqual(false); - expect(hasResultInUserRelation).toEqual(false); - - for (const [relationName, fieldsRelation] of ObjectTyped.entries( - relationField - )) { - const check = fieldsRelation.some((field) => { - const targetItem = userWithRelation[relationName]; - const target = - targetItem instanceof Collection - ? targetItem.getItems().at(0) - : targetItem; - if (!target) return true; - - // @ts-ignore - return !ObjectTyped.keys(target).includes(field); - }); - expect(check).toEqual(false); - } + it('getProps', () => { + const result = getProps(entityMetadataToken.get(Users)); + expect(result.includes('id')).toBe(true); + expect(result.includes('lastName')).toBe(true); + expect(result.includes('createdAt')).toBe(true); + expect(result.includes('updatedAt')).toBe(true); + expect(result.includes('isActive')).toBe(true); + expect(result.includes('login')).toBe(true); + expect(result.includes('firstName')).toBe(true); + expect(result.includes('testReal')).toBe(true); + expect(result.includes('testArrayNull')).toBe(true); + expect(result.includes('testDate')).toBe(true); + + expect(result.includes('userGroup' as any)).toBe(false); + expect(result.includes('notes' as any)).toBe(false); + expect(result.includes('comments' as any)).toBe(false); + expect(result.includes('roles' as any)).toBe(false); + expect(result.includes('manager' as any)).toBe(false); + expect(result.includes('addresses' as any)).toBe(false); }); - it('getArrayPropsForEntity', () => { - const result = getArrayPropsForEntity(entityMetadataToken, Users, config); - const check: ArrayPropsForEntity = { - target: { - testReal: true, - testArrayNull: true, - }, - manager: { - testReal: true, - testArrayNull: true, - }, - comments: {}, - notes: {}, - userGroup: {}, - roles: {}, - addresses: { - arrayField: true, - }, - }; - expect(result).toEqual(check); - }); + it('getPropsType', () => { + const result = getPropsType(entityMetadataToken.get(Users), config); - it('getArrayFields', () => { - const result = getArrayFields(entityMetadataToken.get(Addresses), config); expect(result).toEqual({ - arrayField: true, - } as PropsArray); + createdAt: 'date', + firstName: 'string', + id: 'number', + isActive: 'boolean', + lastName: 'string', + login: 'string', + testArrayNull: 'array', + testDate: 'date', + testReal: 'array', + updatedAt: 'date', + }); }); - it('getFieldWithType', () => { - const result = getFieldWithType(entityMetadataToken.get(Addresses), config); - expect(result.arrayField).toBe('array'); - expect(result.state).toBe('string'); - expect(result.id).toBe('number'); - expect(result.createdAt).toBe('date'); - const result2 = getFieldWithType(entityMetadataToken.get(Users), config); - - expect(result2.isActive).toBe('boolean'); + it('getPropsNullable', () => { + const result = getPropsNullable(entityMetadataToken.get(Users)); + expect(result).toEqual([ + 'firstName', + 'testReal', + 'testArrayNull', + 'lastName', + 'isActive', + 'testDate', + 'createdAt', + 'updatedAt', + ]); }); - it('getTypeForAllProps', () => { - const result = getTypeForAllProps(entityMetadataToken, Users, config); - expect(result.manager.id).toBe(TypeField.number); - expect(result.testDate).toBe(TypeField.date); - // @ts-ignore - expect(result.comments.id).toBe(TypeField.number); - // @ts-ignore - expect(result.notes.id).toBe(TypeField.string); + it('getPrimaryColumnName', () => { + const result = getPrimaryColumnName(entityMetadataToken.get(Users)); + expect(result).toBe('id'); }); - it('getRelationType', () => { - const result = getRelationTypeArray(entityMetadataToken.get(Users)); - expect(result.roles).toBe(true); - expect(result.comments).toBe(true); - expect(result.manager).toBe(false); - expect(result.addresses).toBe(false); - expect(result.userGroup).toBe(false); - expect(result.notes).toBe(true); + it('getPrimaryColumnType', () => { + const result = getPrimaryColumnType(entityMetadataToken.get(Users)); + expect(result).toBe(TypeField.number); }); - it('getTypePrimaryColumn', () => { - expect(getTypePrimaryColumn(entityMetadataToken.get(Users))).toBe( - TypeField.number - ); - expect(getTypePrimaryColumn(entityMetadataToken.get(Notes))).toBe( - TypeField.string - ); + it('getRelation', () => { + const result = getRelation(entityMetadataToken.get(Users)); + expect(result.includes('id' as any)).toBe(false); + expect(result.includes('lastName' as any)).toBe(false); + expect(result.includes('createdAt' as any)).toBe(false); + expect(result.includes('updatedAt' as any)).toBe(false); + expect(result.includes('isActive' as any)).toBe(false); + expect(result.includes('login' as any)).toBe(false); + expect(result.includes('firstName' as any)).toBe(false); + expect(result.includes('testReal' as any)).toBe(false); + expect(result.includes('testArrayNull' as any)).toBe(false); + expect(result.includes('testDate' as any)).toBe(false); + + expect(result.includes('userGroup')).toBe(true); + expect(result.includes('notes')).toBe(true); + expect(result.includes('comments')).toBe(true); + expect(result.includes('roles')).toBe(true); + expect(result.includes('manager')).toBe(true); + expect(result.includes('addresses')).toBe(true); }); - it('getPropsFromDb', () => { - const result = getPropsFromDb(entityMetadataToken.get(Users), config); - // testReal has isNullable false but have default should be true - expect(result['testReal']).toEqual({ - type: 'real', - isArray: true, - isNullable: true, - }); - - const result2 = getPropsFromDb(entityMetadataToken.get(Roles), config); - expect(result2['key']).toEqual({ - type: 'varchar', - isArray: false, - isNullable: false, + it('getRelationProperty', () => { + const result = getRelationProperty(entityMetadataToken.get(Users)); + expect(result).toEqual({ + addresses: { + entityClass: Addresses, + isArray: false, + nullable: true, + }, + comments: { + entityClass: Comments, + isArray: true, + nullable: false, + }, + manager: { + entityClass: Users, + isArray: false, + nullable: true, + }, + notes: { + entityClass: Notes, + isArray: true, + nullable: false, + }, + roles: { + entityClass: Roles, + isArray: true, + nullable: false, + }, + userGroup: { + entityClass: UserGroups, + isArray: false, + nullable: true, + }, }); }); - - it('getRelationTypeName', () => { - const result = getRelationTypeName(entityMetadataToken.get(Users)); - expect(result.roles).toBe('Roles'); - expect(result.comments).toBe('Comments'); - expect(result.manager).toBe('Users'); - expect(result.addresses).toBe('Addresses'); - expect(result.userGroup).toBe('UserGroups'); - expect(result.notes).toBe('Notes'); - }); - - it('getRelationTypePrimaryColumn', () => { - const result = getRelationTypePrimaryColumn(entityMetadataToken, Users); - expect(result.roles).toBe(TypeField.number); - expect(result.comments).toBe(TypeField.number); - expect(result.manager).toBe(TypeField.number); - expect(result.addresses).toBe(TypeField.number); - expect(result.userGroup).toBe(TypeField.number); - expect(result.notes).toBe(TypeField.string); - }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts index 28ed46bc..1ad4cffc 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-helper/index.ts @@ -1,126 +1,34 @@ -import { - EntityKey, - EntityMetadata, - EntityName, - MetadataStorage, - ReferenceKind, -} from '@mikro-orm/core'; -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { EntityKey, EntityMetadata } from '@mikro-orm/core'; import { ObjectLiteral, ResultMicroOrmModuleOptions } from '../../../types'; import { - AllFieldWithType, - ArrayPropsForEntity, FieldWithType, - PropsArray, - PropsForField, - RelationPrimaryColumnType, - RelationPropsArray, - RelationPropsTypeName, - RelationTree, - ResultGetField, + FilterNullableProps, + RelationProperty, + TupleOfEntityProps, + TupleOfEntityRelation, TypeField, TypeForId, } from '../../mixin/types'; -import { getEntityName } from '../../mixin/helper'; +export const getRelation = ( + entityMetadata: EntityMetadata +) => entityMetadata.relations.map((i) => i.name) as TupleOfEntityRelation; -export const getField = ( +export const getProps = ( entityMetadata: EntityMetadata -): ResultGetField => { - const relations = entityMetadata.relations.map((i) => i.name); +): TupleOfEntityProps => { + const relations = getRelation(entityMetadata); - const field = entityMetadata.props + return entityMetadata.props .map((i) => i.name) - .filter((i) => !relations.includes(i)); - - return { - relations, - field, - } as unknown as ResultGetField; + .filter((i) => !relations.includes(i)) as TupleOfEntityProps; }; -export const getPropsTreeForRepository = ( - metadataStorage: MetadataStorage, - entity: EntityName -): RelationTree => { - const entityMetadata = metadataStorage.get(entity); - - const relationType = entityMetadata.relations.reduce((acum, item) => { - acum[item.name] = item.entity(); - return acum; - }, {} as Record, EntityName>); - - return ObjectTyped.entries(relationType).reduce( - (acum, [key, value]) => ({ - ...acum, - ...{ [key]: getField(metadataStorage.get(value))['field'] }, - }), - {} as RelationTree - ); -}; -export const getArrayPropsForEntity = ( - metadataStorage: MetadataStorage, - entity: EntityName, - config: ResultMicroOrmModuleOptions['options']['arrayType'] -): ArrayPropsForEntity => { - const currentMetadata = metadataStorage.get(entity); - - const relationsArrayFields = currentMetadata.relations.reduce( - (acum, item) => { - const entityMetadata = metadataStorage.get(item.entity()); - acum[item.name] = getArrayFields(entityMetadata, config) as any; - return acum; - }, - {} as any - ); - - return { - target: getArrayFields(currentMetadata, config), - ...relationsArrayFields, - } as ArrayPropsForEntity; -}; - -export const getArrayFields = ( - entityMetadata: EntityMetadata, - config: ResultMicroOrmModuleOptions['options']['arrayType'] -): PropsArray => { - return ObjectTyped.entries(entityMetadata.properties).reduce( - (acum, [name, val]) => { - if (config.includes(val['type'])) { - acum[name] = true; - } - return acum; - }, - {} as Record, boolean> - ) as unknown as PropsArray; -}; - -export const getTypeForAllProps = ( - metadataStorage: MetadataStorage, - entity: EntityName, - config: ResultMicroOrmModuleOptions['options']['arrayType'] -): AllFieldWithType => { - const currentMetadata = metadataStorage.get(entity); - - const targetField = getFieldWithType(currentMetadata, config); - - const relationField = currentMetadata.relations.reduce((acum, item) => { - const entityMetadata = metadataStorage.get(item.entity()); - acum[item.name] = getFieldWithType(entityMetadata, config) as any; - return acum; - }, {} as any); - - return { - ...targetField, - ...relationField, - }; -}; - -export const getFieldWithType = ( +export const getPropsType = ( entityMetadata: EntityMetadata, config: ResultMicroOrmModuleOptions['options']['arrayType'] ): FieldWithType => { - const { field } = getField(entityMetadata); + const field = getProps(entityMetadata); const result = {} as any; for (const item of field) { @@ -155,19 +63,23 @@ export const getFieldWithType = ( return result; }; -export const getRelationTypeArray = ( +export const getPropsNullable = ( entityMetadata: EntityMetadata -): RelationPropsArray => { - const typeArray = [ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY]; - - const result = {} as any; - for (const item of entityMetadata.relations) { - result[item.name] = typeArray.includes(item.kind); - } - return result; +): FilterNullableProps> => { + return getProps(entityMetadata) + .map((i) => { + // @ts-ignore + const props = entityMetadata.properties[i]; + return props.nullable || props.default !== undefined ? i : false; + }) + .filter((i) => !!i) as FilterNullableProps>; }; -export const getTypePrimaryColumn = ( +export const getPrimaryColumnName = ( + entityMetadata: EntityMetadata +) => entityMetadata.getPrimaryProp().name.toString(); + +export const getPrimaryColumnType = ( entityMetadata: EntityMetadata ): TypeForId => { return entityMetadata.getPrimaryProp().runtimeType === 'number' @@ -175,44 +87,20 @@ export const getTypePrimaryColumn = ( : TypeField.string; }; -export const getPropsFromDb = ( - entityMetadata: EntityMetadata, - config: ResultMicroOrmModuleOptions['options']['arrayType'] -): PropsForField => { - return getField(entityMetadata)['field'].reduce((acum, fieldName: any) => { - // @ts-ignore - const props = entityMetadata.properties[fieldName]; - const isArray = config.includes(props['type']); - let type = props.type; - if (isArray) { - type = props.columnTypes.at(0).split('[').at(0); - } - - acum[props.name] = { - type: type, - isArray: isArray, - isNullable: props.nullable || props.default !== undefined, - }; - return acum; - }, {} as any) as PropsForField; -}; - -export const getRelationTypeName = ( +export const getRelationProperty = ( entityMetadata: EntityMetadata -): RelationPropsTypeName => { - return entityMetadata.relations.reduce((acum, i) => { - acum[i.name] = getEntityName(i.entity() as any); - return acum; - }, {} as Record) as RelationPropsTypeName; -}; - -export const getRelationTypePrimaryColumn = ( - metadataStorage: MetadataStorage, - entity: EntityName -): RelationPrimaryColumnType => { - return metadataStorage.get(entity).relations.reduce((acum, item) => { - acum[item.name] = getTypePrimaryColumn(metadataStorage.get(item.entity())); +): RelationProperty => { + return entityMetadata.relations.reduce((acum, item) => { + // @ts-expect-error its dynamic creater + acum[item.name] = { + entityClass: item.entity() as any, + nullable: + item.kind === 'm:n' || item.kind === '1:m' + ? false + : (!!item.nullable as any), + isArray: item.kind === 'm:n' || item.kind === ('1:m' as any), + }; return acum; - }, {} as Record) as RelationPrimaryColumnType; + }, {} as RelationProperty); }; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts index 81ed1e14..367396c8 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts @@ -1,8 +1,10 @@ import { EntityRelation, + QueryField, ResourceObject, ResourceObjectRelationships, } from '@klerick/json-api-nestjs-shared'; +import { Inject } from '@nestjs/common'; import { ObjectLiteral } from '../../../types'; import { OrmService } from '../../mixin/types'; @@ -15,52 +17,190 @@ import { QueryOne, } from '../../mixin/zod'; +import { + getAll, + getOne, + deleteOne, + postOne, + patchOne, + getRelationship, + deleteRelationship, + patchRelationship, + postRelationship, +} from '../orm-methods'; +import { MicroOrmUtilService } from './micro-orm-util.service'; +import { JsonApiTransformerService } from '../../mixin/service/json-api-transformer.service'; + export class MicroOrmService implements OrmService { - postOne(inputData: PostData): Promise> { - return {} as any; + @Inject(MicroOrmUtilService) microOrmUtilService!: MicroOrmUtilService; + @Inject(JsonApiTransformerService) + jsonApiTransformerService!: JsonApiTransformerService; + + async getAll(query: Query): Promise> { + const { page } = query; + const { totalItems, items } = await getAll.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, query); + + const { data, included } = this.jsonApiTransformerService.transformData( + items, + query + ); + + const meta = { + totalItems: totalItems, + pageNumber: page.number, + pageSize: page.size, + }; + + return { + meta, + data, + ...(included ? { included } : {}), + }; } - patchOne( + + async getOne( id: number | string, - inputData: PatchData + query: QueryOne ): Promise> { - return {} as any; + const result = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, query); + const { data, included } = this.jsonApiTransformerService.transformData( + result, + query + ); + return { + meta: {}, + data, + ...(included ? { included } : {}), + }; } - getOne(id: number | string, query: QueryOne): Promise> { - return {} as any; + async deleteOne(id: number | string): Promise { + await deleteOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id); } - getAll(query: Query): Promise> { - return {} as any; + async postOne(inputData: PostData): Promise> { + const result = await postOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, inputData); + + const { relationships } = inputData; + const fakeQuery: Query = { + [QueryField.fields]: null, + [QueryField.include]: Object.keys(relationships || {}), + } as any; + + const { data, included } = this.jsonApiTransformerService.transformData( + result, + fakeQuery + ); + + return { + meta: {}, + data, + ...(included ? { included } : {}), + }; + } + async patchOne( + id: number | string, + inputData: PatchData + ): Promise> { + const result = await patchOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, inputData); + + const { relationships } = inputData; + const fakeQuery: Query = { + [QueryField.fields]: null, + [QueryField.include]: Object.keys(relationships || {}), + } as any; + + const { data, included } = this.jsonApiTransformerService.transformData( + result, + fakeQuery + ); + + return { + meta: {}, + data, + ...(included ? { included } : {}), + }; } - async deleteOne(id: number | string): Promise {} + async getRelationship>( + id: number | string, + rel: Rel + ): Promise> { + const result = await getRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel); - postRelationship>( + return { + meta: {}, + data: this.jsonApiTransformerService.transformRel(result, rel), + }; + } + + async deleteRelationship>( id: number | string, rel: Rel, input: PostRelationshipData - ): Promise> { - return {} as any; + ): Promise { + await deleteRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); } - getRelationship>( + async postRelationship>( id: number | string, - rel: Rel + rel: Rel, + input: PostRelationshipData ): Promise> { - return {} as any; + const result = await postRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + + return { + meta: {}, + data: this.jsonApiTransformerService.transformRel(result, rel), + }; } - patchRelationship>( + async patchRelationship>( id: number | string, rel: Rel, input: PatchRelationshipData ): Promise> { - return {} as any; + const result = await patchRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel, input); + + return { + meta: {}, + data: this.jsonApiTransformerService.transformRel(result, rel), + }; } - async deleteRelationship>( - id: number | string, - rel: Rel, - input: PostRelationshipData - ): Promise {} } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts index ca42707e..5e29384f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts @@ -1,13 +1,13 @@ import { FactoryProvider, ValueProvider } from '@nestjs/common'; import { - PARAMS_FOR_ZOD_SCHEMA, ZOD_INPUT_QUERY_SCHEMA, ZOD_QUERY_SCHEMA, ZOD_POST_SCHEMA, ZOD_PATCH_SCHEMA, ZOD_POST_RELATIONSHIP_SCHEMA, ZOD_PATCH_RELATIONSHIP_SCHEMA, + ENTITY_MAP_PROPS, } from '../../../constants'; import { @@ -24,45 +24,169 @@ import { zodPatchRelationship, ZodPatchRelationship, } from '../zod'; -import { ObjectLiteral } from '../../../types'; -import { EntityProps, ZodParams } from '../types'; +import { EntityClass, ObjectLiteral } from '../../../types'; +import { + AllFieldWithType, + ArrayPropsForEntity, + EntityProps, + FieldWithType, + PropsForField, + RelationPrimaryColumnType, + RelationPropsArray, + RelationPropsTypeName, + RelationTree, + ResultGetField, + ZodEntityProps, +} from '../types'; +import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; + +function getEntityMap( + entityMapProps: Map, ZodEntityProps>, + entity: Function & { prototype: E } +) { + const entityMap = entityMapProps.get(entity); + if (!entityMap) throw new Error('Entity not found in map'); + return entityMap; +} + +export function getParamsForOatchANdPostZod( + entityMapProps: Map, ZodEntityProps>, + entity: Function & { prototype: E } +) { + const entityMap = getEntityMap(entityMapProps, entity); + + const { + primaryColumnType, + typeName, + propsType, + primaryColumnName, + propsNullable, + relationProperty, + } = entityMap; + const fieldWithType = {} as FieldWithType; + const propsDb = {} as PropsForField; + const relationArrayProps = {} as RelationPropsArray; + const relationPopsName = {} as RelationPropsTypeName; + const primaryColumnTypeForRel = {} as RelationPrimaryColumnType; + + for (const [name, type] of ObjectTyped.entries(propsType)) { + Reflect.set(propsDb, name, { + type: type, + isArray: type === 'array', + isNullable: (propsNullable as any[]).includes(name), + }); + if (name === primaryColumnName) continue; + Reflect.set(fieldWithType, name, type); + } + + for (const [name, value] of ObjectTyped.entries(relationProperty)) { + const relEntityMap = getEntityMap(entityMapProps, value.entityClass); + + Reflect.set(relationArrayProps, name, value.isArray); + Reflect.set(relationPopsName, name, relEntityMap.className); + Reflect.set(primaryColumnTypeForRel, name, relEntityMap.primaryColumnType); + } + + return { + primaryColumnType, + typeName: typeName as any, + fieldWithType, + propsDb: propsDb as PropsForField, + primaryColumnName: primaryColumnName as EntityProps, + relationArrayProps, + relationPopsName, + primaryColumnTypeForRel, + }; +} -export function ZodInputQuerySchema(): FactoryProvider< - ZodInputQuery -> { +export function ZodInputQuerySchema( + entity: EntityClass +): FactoryProvider> { return { provide: ZOD_INPUT_QUERY_SCHEMA, inject: [ { - token: PARAMS_FOR_ZOD_SCHEMA, + token: ENTITY_MAP_PROPS, optional: false, }, ], - useFactory: (zodParams: ZodParams>) => { - const { entityFieldsStructure, entityRelationStructure } = zodParams; - return zodInputQuery(entityFieldsStructure, entityRelationStructure); + useFactory: (entityMapProps: Map, ZodEntityProps>) => { + const entityMap = getEntityMap(entityMapProps, entity); + + const { props, relations, relationProperty } = entityMap; + + const entityRelationStructure = ObjectTyped.entries( + relationProperty + ).reduce((acum, [name, value]) => { + const relMap = getEntityMap(entityMapProps, value.entityClass); + Reflect.set(acum, name, relMap.props); + return acum; + }, {} as RelationTree); + + return zodInputQuery( + { + field: props, + relations: relations, + } as ResultGetField, + entityRelationStructure + ); }, }; } -export function ZodQuerySchema(): FactoryProvider< - ZodQuery -> { +export function ZodQuerySchema( + entity: EntityClass +): FactoryProvider> { return { provide: ZOD_QUERY_SCHEMA, inject: [ { - token: PARAMS_FOR_ZOD_SCHEMA, + token: ENTITY_MAP_PROPS, optional: false, }, ], - useFactory: (zodParams: ZodParams>) => { + useFactory: (entityMapProps: Map, ZodEntityProps>) => { + const entityMap = getEntityMap(entityMapProps, entity); + const { - entityFieldsStructure, - entityRelationStructure, - propsType, - propsArray, - } = zodParams; + props, + relations, + relationProperty, + propsType: propsTypeEntity, + } = entityMap; + + const propsType = { ...propsTypeEntity } as AllFieldWithType; + + const propsArray = { target: {} } as ArrayPropsForEntity; + + for (const [name, type] of ObjectTyped.entries(propsTypeEntity)) { + if (type !== 'array') continue; + Reflect.set(propsArray.target, name, true); + } + + const entityRelationStructure = {} as RelationTree; + for (const [name, value] of ObjectTyped.entries(relationProperty)) { + const relMap = getEntityMap(entityMapProps, value.entityClass); + + if (!(name in propsArray)) { + Reflect.set(propsArray, name, {}); + } + for (const [relNameField, type] of ObjectTyped.entries( + relMap.propsType + )) { + if (type !== 'array') continue; + const propsArrayObject = Reflect.get(propsArray, name); + Reflect.set(propsArrayObject, relNameField, true); + } + + Reflect.set(propsType, name, relMap.propsType); + Reflect.set(entityRelationStructure, name, relMap.props); + } + const entityFieldsStructure = { + field: props, + relations: relations, + } as ResultGetField; + return zodQuery( entityFieldsStructure, entityRelationStructure, @@ -73,75 +197,75 @@ export function ZodQuerySchema(): FactoryProvider< }; } -export function ZodPostSchema< - E extends ObjectLiteral, - I extends string ->(): FactoryProvider> { +export function ZodPostSchema( + entity: EntityClass +): FactoryProvider> { return { provide: ZOD_POST_SCHEMA, inject: [ { - token: PARAMS_FOR_ZOD_SCHEMA, + token: ENTITY_MAP_PROPS, optional: false, }, ], - useFactory: (zodParams: ZodParams, I>) => { + useFactory: (entityMapProps: Map, ZodEntityProps>) => { const { - typeId, + primaryColumnType, typeName, fieldWithType, propsDb, - primaryColumn, + primaryColumnName, relationArrayProps, relationPopsName, - primaryColumnType, - } = zodParams; + primaryColumnTypeForRel, + } = getParamsForOatchANdPostZod(entityMapProps, entity); + return zodPost( - typeId, + primaryColumnType, typeName, fieldWithType, propsDb, - primaryColumn, + primaryColumnName, relationArrayProps, relationPopsName, - primaryColumnType + primaryColumnTypeForRel ); }, }; } -export function ZodPatchSchema< - E extends ObjectLiteral, - I extends string ->(): FactoryProvider> { +export function ZodPatchSchema( + entity: EntityClass +): FactoryProvider> { return { provide: ZOD_PATCH_SCHEMA, inject: [ { - token: PARAMS_FOR_ZOD_SCHEMA, + token: ENTITY_MAP_PROPS, optional: false, }, ], - useFactory: (zodParams: ZodParams, I>) => { + useFactory: (entityMapProps: Map, ZodEntityProps>) => { const { - typeId, + primaryColumnType, typeName, fieldWithType, propsDb, - primaryColumn, + primaryColumnName, relationArrayProps, relationPopsName, - primaryColumnType, - } = zodParams; + primaryColumnTypeForRel, + } = getParamsForOatchANdPostZod(entityMapProps, entity); + return zodPatch( - typeId, - typeName, + primaryColumnType, + typeName as I, fieldWithType, propsDb, - primaryColumn, + primaryColumnName as EntityProps, relationArrayProps, relationPopsName, - primaryColumnType + primaryColumnTypeForRel ); }, }; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts index 5dc4e8f1..0326a314 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/mixin.module.ts @@ -23,6 +23,7 @@ import { ZodInputPostRelationshipSchema, } from './factory'; import { SwaggerBindService } from './swagger'; +import { JsonApiTransformerService } from './service/json-api-transformer.service'; export class MixinModule { static forRoot(options: MixinOptions): DynamicModule { @@ -71,11 +72,12 @@ export class MixinModule { currentEntityProvider, findOneRowEntityProvider, checkRelationNameProvider, + JsonApiTransformerService, ...ormModule.getUtilProviders(entity), - ZodInputQuerySchema(), - ZodQuerySchema(), - ZodPatchSchema(), - ZodPostSchema(), + ZodInputQuerySchema(entity), + ZodQuerySchema(entity), + ZodPatchSchema(entity), + ZodPostSchema(entity), SwaggerBindService, ZodInputPatchRelationshipSchema, ZodInputPostRelationshipSchema, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts index d4c5d359..293687bb 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts @@ -18,6 +18,7 @@ export class QueryPipe transform(value: InputQuery): Query { try { + console.log(JSON.stringify(value)); return this.zodQuerySchema.parse(value); } catch (e) { if (e instanceof ZodError) { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts index 092f8f85..0a1d527f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-one.ts @@ -1,24 +1,25 @@ -import { EntityClass, EntityTarget, ObjectLiteral } from '../../../../types'; import { Type } from '@nestjs/common'; -import { EntityProps, TypeField, ZodParams } from '../../types'; import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; -import { errorSchema } from '../utils'; + +import { TypeField, ZodEntityProps } from '../../types'; +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { errorSchema, getEntityMapProps } from '../utils'; export function deleteOne( controller: Type, descriptor: PropertyDescriptor, entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, methodName: string ) { const entityName = entity.name; - const { typeId } = zodParams; + const { primaryColumnType } = getEntityMapProps(mapEntity, entity); ApiParam({ name: 'id', required: true, - type: typeId === TypeField.number ? 'integer' : 'string', + type: primaryColumnType === TypeField.number ? 'integer' : 'string', description: `ID of resource "${entityName}"`, })(controller, methodName, descriptor); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts index b78b2c38..2b1c2b8b 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/delete-relationship.ts @@ -1,11 +1,12 @@ -import { EntityClass, ObjectLiteral } from '../../../../types'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { generateSchema } from '@anatine/zod-openapi'; import { Type } from '@nestjs/common'; -import { EntityProps, TypeField, ZodParams } from '../../types'; +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { TypeField, ZodEntityProps } from '../../types'; import { zodPatchRelationship } from '../../zod'; -import { errorSchema } from '../utils'; -import { generateSchema } from '@anatine/zod-openapi'; +import { errorSchema, getEntityMapProps } from '../utils'; + import { ReferenceObject, SchemaObject, @@ -15,15 +16,12 @@ export function deleteRelationship( controller: Type, descriptor: PropertyDescriptor, entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, methodName: string ) { const entityName = entity.name; - const { - entityFieldsStructure: { relations }, - typeId, - } = zodParams; + const { relations, primaryColumnType } = getEntityMapProps(mapEntity, entity); ApiOperation({ summary: `Delete list of relation for resource "${entityName}"`, @@ -33,7 +31,7 @@ export function deleteRelationship( ApiParam({ name: 'id', required: true, - type: typeId === TypeField.number ? 'integer' : 'string', + type: primaryColumnType === TypeField.number ? 'integer' : 'string', description: `ID of resource "${entityName}"`, })(controller, methodName, descriptor); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts index a5ed5e39..cc60cd6e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts @@ -3,24 +3,25 @@ import { Type } from '@nestjs/common'; import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; import { EntityClass, ObjectLiteral } from '../../../../types'; -import { EntityProps, ZodParams } from '../../types'; -import { errorSchema, jsonSchemaResponse } from '../utils'; +import { ZodEntityProps } from '../../types'; +import { errorSchema, getEntityMapProps, jsonSchemaResponse } from '../utils'; import { DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE } from '../../../../constants'; export function getAll( controller: Type, descriptor: PropertyDescriptor, entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, methodName: string ): void { - const { entityFieldsStructure, entityRelationStructure, primaryColumn } = - zodParams; - const { field, relations } = entityFieldsStructure; - - const relationTree = ObjectTyped.entries(entityRelationStructure).reduce( - (acum, [name, filed]) => { - acum.push(...filed.map((i) => `${name.toLocaleString()}.${i}`)); + const { props, relations, relationProperty, primaryColumnName } = + getEntityMapProps(mapEntity, entity); + const entityRelationStructure = {} as any; + const relationTree = ObjectTyped.entries(relationProperty).reduce( + (acum, [name, props]) => { + const relMap = getEntityMapProps(mapEntity, props.entityClass); + acum.push(...relMap.props.map((i) => `${name.toLocaleString()}.${i}`)); + entityRelationStructure[name] = relMap.props; return acum; }, [] as string[] @@ -44,7 +45,7 @@ export function getAll( summary: 'Select all field', description: 'Select field for target and relation', value: { - target: field.join(','), + target: props.join(','), ...ObjectTyped.entries(entityRelationStructure).reduce( (acum, [name, props]) => { acum[name.toString()] = props.join(','); @@ -58,7 +59,7 @@ export function getAll( summary: 'Select ids for target', description: 'Select ids for target', value: { - target: field.filter((i) => i === primaryColumn).join(','), + target: props.filter((i) => i === primaryColumnName).join(','), }, }, }, @@ -77,10 +78,10 @@ export function getAll( summary: 'Several conditional', description: 'Get if relation is not null', value: { - [field[0]]: { + [props[0]]: { in: '1,2,3', }, - [field[1]]: { + [props[1]]: { lt: '1', }, [relationTree[0]]: { @@ -124,7 +125,7 @@ export function getAll( let sortSeveral = { summary: 'Sort several field', description: 'Sort several field', - value: `${field[1]},-${field[0]}`, + value: `${props[1]},-${props[0]}`, }; if (relations.length > 0) { @@ -162,7 +163,7 @@ export function getAll( sortSeveral = { summary: 'Sort several field with relation', description: 'Sort several field relation', - value: `${field[1]},-${relationTree[2]},${relationTree[1]},-${field[0]}`, + value: `${props[1]},-${relationTree[2]},${relationTree[1]},-${props[0]}`, }; } @@ -175,12 +176,12 @@ export function getAll( sortAsc: { summary: 'Sort field by ASC', description: 'Sort field by ASC', - value: field[1], + value: props[1], }, sortDesc: { summary: 'Sort field by DESC', description: 'Sort field by DESC', - value: `-${field[1]}`, + value: `-${props[1]}`, }, sortAscRelation, sortDescRelation, @@ -221,6 +222,6 @@ export function getAll( ApiResponse({ status: 200, description: 'Resource list received successfully', - schema: jsonSchemaResponse(entity, zodParams, true), + schema: jsonSchemaResponse(entity, mapEntity, true), })(controller, methodName, descriptor); } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts index f28d93ae..2b0957c1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts @@ -1,25 +1,36 @@ -import { EntityClass, EntityTarget, ObjectLiteral } from '../../../../types'; -import { Type } from '@nestjs/common'; -import { EntityProps, TypeField, ZodParams } from '../../types'; import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { errorSchema, jsonSchemaResponse } from '../utils'; import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { Type } from '@nestjs/common'; + +import { TypeField, ZodEntityProps } from '../../types'; +import { errorSchema, getEntityMapProps, jsonSchemaResponse } from '../utils'; +import { EntityClass, ObjectLiteral } from '../../../../types'; export function getOne( controller: Type, descriptor: PropertyDescriptor, entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, methodName: string ) { const entityName = entity.name; + const { - entityFieldsStructure, - entityRelationStructure, - primaryColumn, - typeId, - } = zodParams; - const { field, relations } = entityFieldsStructure; + props, + relations, + relationProperty, + primaryColumnName, + primaryColumnType, + } = getEntityMapProps(mapEntity, entity); + + const entityRelationStructure = ObjectTyped.entries(relationProperty).reduce( + (acum, [name, props]) => { + const relMap = getEntityMapProps(mapEntity, props.entityClass); + acum[name] = relMap.props; + return acum; + }, + {} as any + ); ApiOperation({ summary: `Get one item of resource "${entityName}"`, @@ -29,7 +40,7 @@ export function getOne( ApiParam({ name: 'id', required: true, - type: typeId === TypeField.number ? 'integer' : 'string', + type: primaryColumnType === TypeField.number ? 'integer' : 'string', description: `ID of resource "${entityName}"`, })(controller, methodName, descriptor); @@ -45,7 +56,7 @@ export function getOne( summary: 'Select all field', description: 'Select field for target and relation', value: { - target: field.join(','), + target: props.join(','), ...ObjectTyped.entries(entityRelationStructure).reduce( (acum, [name, props]) => { acum[name.toString()] = props.join(','); @@ -59,7 +70,7 @@ export function getOne( summary: 'Select ids for target', description: 'Select ids for target', value: { - target: field.filter((i) => i === primaryColumn).join(','), + target: props.filter((i) => i === primaryColumnName).join(','), }, }, }, @@ -103,6 +114,6 @@ export function getOne( ApiResponse({ status: 200, description: 'Resource one item received successfully', - schema: jsonSchemaResponse(entity, zodParams), + schema: jsonSchemaResponse(entity, mapEntity), })(controller, methodName, descriptor); } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts index 1396062f..ce881025 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-relationship.ts @@ -1,22 +1,24 @@ -import { EntityClass, EntityTarget, ObjectLiteral } from '../../../../types'; import { Type } from '@nestjs/common'; -import { EntityProps, TypeField, ZodParams } from '../../types'; -import { errorSchema, schemaTypeForRelation } from '../utils'; import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { EntityClass, ObjectLiteral } from '../../../../types'; +import { TypeField, ZodEntityProps } from '../../types'; +import { + errorSchema, + getEntityMapProps, + schemaTypeForRelation, +} from '../utils'; + export function getRelationship( controller: Type, descriptor: PropertyDescriptor, entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, methodName: string ) { const entityName = entity.name; - const { - entityFieldsStructure: { relations }, - typeId, - } = zodParams; + const { relations, primaryColumnType } = getEntityMapProps(mapEntity, entity); ApiOperation({ summary: `Get list of relation for resource "${entityName}"`, @@ -26,7 +28,7 @@ export function getRelationship( ApiParam({ name: 'id', required: true, - type: typeId === TypeField.number ? 'integer' : 'string', + type: primaryColumnType === TypeField.number ? 'integer' : 'string', description: `ID of resource "${entityName}"`, })(controller, methodName, descriptor); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts index 37a96d54..24113795 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-one.ts @@ -7,28 +7,30 @@ import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; import { generateSchema } from '@anatine/zod-openapi'; import { EntityClass, ObjectLiteral } from '../../../../types'; -import { EntityProps, TypeField, ZodParams } from '../../types'; +import { TypeField, ZodEntityProps } from '../../types'; import { errorSchema, jsonSchemaResponse } from '../utils'; import { zodPatch } from '../../zod'; +import { getParamsForOatchANdPostZod } from '../../factory'; export function patchOne( controller: Type, descriptor: PropertyDescriptor, entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, methodName: string ) { const entityName = entity.name; + const { - typeId, + primaryColumnType, typeName, fieldWithType, propsDb, - primaryColumn, + primaryColumnName, relationArrayProps, relationPopsName, - primaryColumnType, - } = zodParams; + primaryColumnTypeForRel, + } = getParamsForOatchANdPostZod(mapEntity, entity); ApiOperation({ summary: `Update item of resource "${entityName}"`, @@ -38,7 +40,7 @@ export function patchOne( ApiParam({ name: 'id', required: true, - type: typeId === TypeField.number ? 'integer' : 'string', + type: primaryColumnType === TypeField.number ? 'integer' : 'string', description: `ID of resource "${entityName}"`, })(controller, methodName, descriptor); @@ -46,14 +48,14 @@ export function patchOne( description: `Json api schema for update "${entityName}" item`, schema: generateSchema( zodPatch( - typeId, + primaryColumnType, typeName, fieldWithType, propsDb, - primaryColumn, + primaryColumnName, relationArrayProps, relationPopsName, - primaryColumnType + primaryColumnTypeForRel ) ) as SchemaObject | ReferenceObject, required: true, @@ -62,7 +64,7 @@ export function patchOne( ApiResponse({ status: 200, description: `Item of resource "${entityName}" has been updated`, - schema: jsonSchemaResponse(entity, zodParams), + schema: jsonSchemaResponse(entity, mapEntity), })(controller, methodName, descriptor); ApiResponse({ diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts index 55e8e3c5..9301fd62 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/patch-relationship.ts @@ -7,23 +7,24 @@ import { generateSchema } from '@anatine/zod-openapi'; import { Type } from '@nestjs/common'; import { EntityClass, ObjectLiteral } from '../../../../types'; -import { EntityProps, TypeField, ZodParams } from '../../types'; -import { errorSchema, schemaTypeForRelation } from '../utils'; +import { TypeField, ZodEntityProps } from '../../types'; +import { + errorSchema, + getEntityMapProps, + schemaTypeForRelation, +} from '../utils'; import { zodPatchRelationship } from '../../zod'; export function patchRelationship( controller: Type, descriptor: PropertyDescriptor, entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, methodName: string ) { const entityName = entity.name; - const { - entityFieldsStructure: { relations }, - typeId, - } = zodParams; + const { relations, primaryColumnType } = getEntityMapProps(mapEntity, entity); ApiOperation({ summary: `Update list of relation for resource "${entityName}"`, @@ -33,7 +34,7 @@ export function patchRelationship( ApiParam({ name: 'id', required: true, - type: typeId === TypeField.number ? 'integer' : 'string', + type: primaryColumnType === TypeField.number ? 'integer' : 'string', description: `ID of resource "${entityName}"`, })(controller, methodName, descriptor); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts index dab98f8a..10c35021 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-one.ts @@ -7,28 +7,30 @@ import { import { Type } from '@nestjs/common'; import { EntityClass, ObjectLiteral } from '../../../../types'; -import { EntityProps, ZodParams } from '../../types'; +import { ZodEntityProps } from '../../types'; import { errorSchema, jsonSchemaResponse } from '../utils'; import { zodPost } from '../../zod'; +import { getParamsForOatchANdPostZod } from '../../factory'; export function postOne( controller: Type, descriptor: PropertyDescriptor, entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, methodName: string ) { const entityName = entity.name; const { - typeId, + primaryColumnType, typeName, fieldWithType, propsDb, - primaryColumn, + primaryColumnName, relationArrayProps, relationPopsName, - primaryColumnType, - } = zodParams; + primaryColumnTypeForRel, + } = getParamsForOatchANdPostZod(mapEntity, entity); + ApiOperation({ summary: `Create item of resource "${entityName}"`, operationId: `${controller.constructor.name}_${methodName}`, @@ -38,14 +40,14 @@ export function postOne( description: `Json api schema for new "${entityName}" item`, schema: generateSchema( zodPost( - typeId, + primaryColumnType, typeName, fieldWithType, propsDb, - primaryColumn, + primaryColumnName, relationArrayProps, relationPopsName, - primaryColumnType + primaryColumnTypeForRel ) ) as SchemaObject | ReferenceObject, required: true, @@ -54,7 +56,7 @@ export function postOne( ApiResponse({ status: 201, description: `Item of resource "${entityName}" has been created`, - schema: jsonSchemaResponse(entity, zodParams), + schema: jsonSchemaResponse(entity, mapEntity), })(controller, methodName, descriptor); ApiResponse({ diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts index 04f5f82e..3e9fe208 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/post-relationship.ts @@ -6,29 +6,30 @@ import { } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; import { ApiBody, ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; -import { errorSchema, schemaTypeForRelation } from '../utils'; +import { + errorSchema, + getEntityMapProps, + schemaTypeForRelation, +} from '../utils'; import { zodPatchRelationship } from '../../zod'; -import { EntityProps, TypeField, ZodParams } from '../../types'; +import { TypeField, ZodEntityProps } from '../../types'; import { EntityClass, ObjectLiteral } from '../../../../types'; export function postRelationship( controller: Type, descriptor: PropertyDescriptor, entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, methodName: string ) { const entityName = entity.name; - const { - entityFieldsStructure: { relations }, - typeId, - } = zodParams; + const { relations, primaryColumnType } = getEntityMapProps(mapEntity, entity); ApiParam({ name: 'id', required: true, - type: typeId === TypeField.number ? 'integer' : 'string', + type: primaryColumnType === TypeField.number ? 'integer' : 'string', description: `ID of resource "${entityName}"`, })(controller, methodName, descriptor); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts index 5e9a49c0..d37778f7 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts @@ -11,11 +11,17 @@ import { JSON_API_CONTROLLER_POSTFIX, JSON_API_DECORATOR_ENTITY, PARAMS_FOR_ZOD_SCHEMA, + ENTITY_MAP_PROPS, } from '../../../constants'; import { getProviderName, nameIt } from '../helper'; import { JsonBaseController } from '../controller/json-base.controller'; import { EntityClass, ObjectLiteral } from '../../../types'; -import { DecoratorOptions, EntityProps, ZodParams } from '../types'; +import { + DecoratorOptions, + EntityProps, + ZodEntityProps, + ZodParams, +} from '../types'; import { FilterOperand } from './filter-operand-model'; import { createApiModels } from './utils'; import { Bindings } from '../config/bindings'; @@ -29,10 +35,10 @@ export class SwaggerBindService @Inject(CURRENT_ENTITY) private entity!: EntityClass; @Inject(DiscoveryService) private discoveryService!: DiscoveryService; @Inject(CONTROL_OPTIONS_TOKEN) private config!: DecoratorOptions; - @Inject(PARAMS_FOR_ZOD_SCHEMA) private zodParams!: ZodParams< - E, - EntityProps, - string + + @Inject(ENTITY_MAP_PROPS) private mapEntity!: Map< + EntityClass, + ZodEntityProps >; onModuleInit(): any { @@ -58,6 +64,11 @@ export class SwaggerBindService ); if (!controllerInst) throw new Error(`Controller for ${this.entity.name} is empty`); + + const mapProps = this.mapEntity.get(this.entity); + if (!mapProps) + throw new Error(`ZodEntityProps for ${this.entity.name} is empty`); + const controller = controllerInst.instance.constructor; const apiTag = Reflect.getMetadata(DECORATORS.API_TAGS, controller); if (!apiTag) { @@ -67,7 +78,7 @@ export class SwaggerBindService ApiTags(this.entity.name)(controller); ApiExtraModels(FilterOperand)(controller); - ApiExtraModels(createApiModels(this.entity, this.zodParams))(controller); + ApiExtraModels(createApiModels(this.entity, mapProps))(controller); const { allowMethod = ObjectTyped.keys(Bindings) } = this.config; for (const method of ObjectTyped.keys(Bindings)) { @@ -88,7 +99,7 @@ export class SwaggerBindService controller.prototype, descriptor, this.entity, - this.zodParams, + this.mapEntity, method ); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts index 4b1b284d..63fa0408 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts @@ -6,7 +6,7 @@ import { camelToKebab, } from '@klerick/json-api-nestjs-shared'; -import { EntityProps, TypeField, ZodParams } from '../types'; +import { EntityProps, TypeField, ZodEntityProps, ZodParams } from '../types'; import { EntityClass, ObjectLiteral } from '../../../types'; export const errorSchema = { @@ -50,19 +50,11 @@ export const errorSchema = { export function jsonSchemaResponse( entity: EntityClass, - zodParams: ZodParams, string>, + mapEntity: Map, ZodEntityProps>, array = false ) { - const { - entityFieldsStructure, - fieldWithType, - relationArrayProps, - relationPopsName, - primaryColumn, - } = zodParams; - const { relations } = entityFieldsStructure; - - const relationTypeName = relationPopsName; + const { propsType, relations, relationProperty, primaryColumnName } = + getEntityMapProps(mapEntity, entity); const dataType = { type: 'object', @@ -76,8 +68,8 @@ export function jsonSchemaResponse( }, attributes: { type: 'object', - properties: ObjectTyped.entries(fieldWithType) - .filter(([name]) => name !== primaryColumn) + properties: ObjectTyped.entries(propsType) + .filter(([name]) => name !== primaryColumnName) .reduce((acum, [name, type]) => { switch (type) { case TypeField.array: @@ -120,9 +112,10 @@ export function jsonSchemaResponse( properties: { type: { type: 'string', - const: camelToKebab( - relationTypeName[name as EntityRelation] - ), + const: getEntityMapProps( + mapEntity, + Reflect.get(relationProperty, name).entityClass + ).typeName, }, id: { type: 'string', @@ -146,7 +139,7 @@ export function jsonSchemaResponse( }, required: ['self'], }, - data: relationArrayProps[name as EntityRelation] + data: Reflect.get(relationProperty, name).isArray ? dataArray : dataItem, }, @@ -230,48 +223,35 @@ export function jsonSchemaResponse( export function createApiModels( entity: EntityClass, - zodParams: ZodParams, string> + mapEntity: ZodEntityProps ): EntityClass { - const { - entityFieldsStructure, - propsType, - relationPopsName, - propsDb, - relationArrayProps, - } = zodParams; + const { propsType, props, relations, propsNullable, relationProperty } = + mapEntity; - for (const [name, type] of ObjectTyped.entries(propsType)) { - const { field, relations } = entityFieldsStructure; + for (const name of props) { let currentType: any; let required = false; let isArray = false; - if (field.includes(name as string)) { - required = !propsDb[name].isNullable; - isArray = propsDb[name].isArray; - switch (propsType[name as EntityProps]) { - case TypeField.date: - currentType = Date; - break; - case TypeField.number: - currentType = Number; - break; - case TypeField.boolean: - currentType = Boolean; - break; - default: - currentType = String; - } + required = !(propsNullable as any).includes(name); + const type = Reflect.get(propsType, name); + isArray = type === 'array'; + switch (type) { + case TypeField.date: + currentType = Date; + break; + case TypeField.number: + currentType = Number; + break; + case TypeField.boolean: + currentType = Boolean; + break; + default: + currentType = String; } - if (relations.includes(name as string)) { - currentType = relationPopsName[name as EntityRelation]; - if (propsDb[name]) { - required = !propsDb[name].isNullable; - isArray = propsDb[name].isArray; - } else { - isArray = relationArrayProps[name as EntityRelation]; - required = !isArray; - } + const propsRel = Reflect.get(relationProperty, name); + currentType = propsRel.entityClass; + isArray = propsRel.isArray; } ApiProperty({ @@ -281,6 +261,15 @@ export function createApiModels( })(entity.prototype, name.toString()); } + for (const name of relations) { + const propsRel = Reflect.get(relationProperty, name); + ApiProperty({ + required: !propsRel.nullable, + isArray: propsRel.isArray, + type: propsRel.entityClass, + })(entity.prototype, name.toString()); + } + return entity; } @@ -310,3 +299,12 @@ export const schemaTypeForRelation = { }, }, }; + +export function getEntityMapProps( + mapEntity: Map, ZodEntityProps>, + entity: EntityClass +) { + const entityMap = mapEntity.get(entity); + if (!entityMap) throw new Error('Entity not found in map'); + return entityMap; +} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts index 23072f39..c6298320 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/zod-types.ts @@ -15,6 +15,7 @@ import { ObjectLiteral as Entity, ObjectLiteral, } from '../../../types'; +import { Collection } from '@mikro-orm/core'; export enum PropsNameResultField { field = 'field', @@ -131,7 +132,11 @@ export type PropsFieldItem = { }; export type RelationPropsArray = { - [K in EntityRelation]: E[K] extends unknown[] ? true : false; + [K in EntityRelation]: E[K] extends unknown[] + ? true + : E[K] extends Collection> + ? true + : false; }; export type RelationPropsTypeName = { @@ -141,3 +146,34 @@ export type RelationPropsTypeName = { export type RelationPrimaryColumnType = { [K in EntityRelation]: TypeForId; }; + +export type FilterNullableProps< + T, + Props extends readonly (keyof T)[] +> = Props extends [infer Head, ...infer Tail] + ? Head extends keyof T + ? null extends T[Head] + ? [Head, ...FilterNullableProps] + : FilterNullableProps + : FilterNullableProps + : []; + +export type RelationProperty = { + [K in TupleOfEntityRelation[number]]: { + entityClass: TypeOfArray>; + nullable: [Extract] extends [never] ? false : true; + isArray: E[K] extends unknown[] ? true : false; + }; +}; + +export type ZodEntityProps = { + props: TupleOfEntityProps; + propsType: FieldWithType; + propsNullable: FilterNullableProps>; + primaryColumnName: I; + primaryColumnType: TypeForId; + typeName: string; + className: string; + relations: TupleOfEntityRelation; + relationProperty: RelationProperty; +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts index 6d2793fd..110b963e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/index.ts @@ -4,3 +4,5 @@ export * from './zod-input-post-schema'; export * from './zod-input-patch-schema'; export * from './zod-input-post-relationship-schema'; export * from './zod-input-patch-relationship-schema'; + +export { Relationships, Data, Attributes, Id, Type } from './zod-share'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts index d5986891..1455300a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.spec.ts @@ -46,14 +46,14 @@ describe('zodFilterInputQuery', () => { const schema = zodFilterInputQuery(userFields, userRelations); const input = { invalidField: { eq: 'should be ignored' }, - login: { eq: 'johndoe' }, + login: { eq: 'johndoe', gte: '123' }, }; const result = schema.parse(input); expect(result).toEqual({ relation: null, - target: { login: { eq: 'johndoe' } }, + target: { login: { eq: 'johndoe', gte: '123' } }, }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts index 425253b8..8963091f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts @@ -15,6 +15,7 @@ import { } from '../../types'; import { zodRelData } from './rel-data'; import { nonEmptyObject } from '../zod-utils'; +import { Users } from '../../../../mock-utils/microrom'; function getZodRuleForData< K extends string, @@ -155,3 +156,6 @@ export type Relationships< T extends ObjectLiteral, K extends true | false = false > = z.infer>; + +const tmp = {} as Relationships; +const r = tmp.roles; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts index 4de29ce2..92f7bb57 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/index.ts @@ -7,15 +7,3 @@ export { getRelationship } from './get-relationship/get-relationship'; export { postRelationship } from './post-relationship/post-relationship'; export { deleteRelationship } from './delete-relationship/delete-relationship'; export { patchRelationship } from './patch-relationship/patch-relationship'; - -// export const MethodsService = { -// getAll, -// getOne, -// deleteOne, -// postOne, -// patchOne, -// getRelationship, -// postRelationship, -// deleteRelationship, -// patchRelationship, -// }; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts index 159bfdc5..817e04f1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts @@ -1,6 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; import { getDataSourceToken } from '@nestjs/typeorm'; +import { QueryField, FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { + BadRequestException, + NotFoundException, + UnprocessableEntityException, +} from '@nestjs/common'; +import { IMemoryDb } from 'pg-mem'; import { Repository } from 'typeorm'; import { @@ -26,17 +32,8 @@ import { } from '../../../constants'; import { TypeormUtilsService } from './typeorm-utils.service'; import { PostData, PostRelationshipData, Query } from '../../mixin/zod'; -import { QueryField, FilterOperand } from '@klerick/json-api-nestjs-shared'; -import { - EXPRESSION, - OperandsMapExpression, - ObjectLiteral as Entity, -} from '../../../types'; -import { - BadRequestException, - NotFoundException, - UnprocessableEntityException, -} from '@nestjs/common'; +import { EXPRESSION, OperandsMapExpression } from '../type'; +import { ObjectLiteral as Entity } from '../../../types'; import { createAndPullSchemaBase } from '../../../mock-utils'; function getDefaultQuery() { @@ -653,7 +650,7 @@ describe('TypeormUtilsService', () => { }; const managerData = { type: 'users', - id: usersData.manager.id.toString(), + id: usersData.manager?.id.toString(), }; const emptyRoles: { id: string; type: string }[] = []; const emptyManager = null; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts index abaec387..146b0687 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts @@ -15,15 +15,13 @@ import { FilterOperand, } from '@klerick/json-api-nestjs-shared'; +import { ObjectLiteral, ValidateQueryError } from '../../../types'; import { - ObjectLiteral, EXPRESSION, OperandMapExpressionForNull, OperandsMapExpression, OperandsMapExpressionForNullRelation, - ValidateQueryError, -} from '../../../types'; - +} from '../type'; import { PatchData, PostData, Query } from '../../mixin/zod'; import { TupleOfEntityRelation, EntityRelation } from '../../mixin/types'; import { getEntityName } from '../../mixin/helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts index 7494c8e4..edea06bc 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts @@ -1,7 +1,5 @@ import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; -import { EntityTarget, ObjectLiteral } from '../../types'; -import { ResultGetField } from '../mixin/types'; - +import { FilterOperand } from '@klerick/json-api-nestjs-shared'; export type TypeOrmParam = { useSoftDelete?: boolean; runInTransaction?: any>( @@ -9,3 +7,28 @@ export type TypeOrmParam = { fn: Func ) => ReturnType; }; + +export const EXPRESSION = 'EXPRESSION'; +export const OperandsMapExpression = { + [FilterOperand.eq]: `= :${EXPRESSION}`, + [FilterOperand.ne]: `<> :${EXPRESSION}`, + [FilterOperand.regexp]: `~* :${EXPRESSION}`, + [FilterOperand.gt]: `> :${EXPRESSION}`, + [FilterOperand.gte]: `>= :${EXPRESSION}`, + [FilterOperand.in]: `IN (:...${EXPRESSION})`, + [FilterOperand.like]: `ILIKE :${EXPRESSION}`, + [FilterOperand.lt]: `< :${EXPRESSION}`, + [FilterOperand.lte]: `<= :${EXPRESSION}`, + [FilterOperand.nin]: `NOT IN (:...${EXPRESSION})`, + [FilterOperand.some]: `&& :${EXPRESSION}`, +}; + +export const OperandMapExpressionForNull = { + [FilterOperand.ne]: 'IS NOT NULL', + [FilterOperand.eq]: 'IS NULL', +}; + +export const OperandsMapExpressionForNullRelation = { + [FilterOperand.ne]: `EXISTS ${EXPRESSION}`, + [FilterOperand.eq]: `NOT EXISTS ${EXPRESSION}`, +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/types/index.ts index 0ad37ef9..9d3aaed5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/types/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/types/index.ts @@ -2,4 +2,3 @@ export * from './config-param'; export * from './module-common.types'; export * from './util-types'; export * from './error.types'; -export * from './operand'; diff --git a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts b/libs/json-api/json-api-nestjs/src/lib/types/operand.ts deleted file mode 100644 index c54b9985..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/types/operand.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { FilterOperand } from '@klerick/json-api-nestjs-shared'; - -export const EXPRESSION = 'EXPRESSION'; -export const OperandsMapExpression = { - [FilterOperand.eq]: `= :${EXPRESSION}`, - [FilterOperand.ne]: `<> :${EXPRESSION}`, - [FilterOperand.regexp]: `~* :${EXPRESSION}`, - [FilterOperand.gt]: `> :${EXPRESSION}`, - [FilterOperand.gte]: `>= :${EXPRESSION}`, - [FilterOperand.in]: `IN (:...${EXPRESSION})`, - [FilterOperand.like]: `ILIKE :${EXPRESSION}`, - [FilterOperand.lt]: `< :${EXPRESSION}`, - [FilterOperand.lte]: `<= :${EXPRESSION}`, - [FilterOperand.nin]: `NOT IN (:...${EXPRESSION})`, - [FilterOperand.some]: `&& :${EXPRESSION}`, -}; - -export const OperandMapExpressionForNull = { - [FilterOperand.ne]: 'IS NOT NULL', - [FilterOperand.eq]: 'IS NULL', -}; - -export const OperandsMapExpressionForNullRelation = { - [FilterOperand.ne]: `EXISTS ${EXPRESSION}`, - [FilterOperand.eq]: `NOT EXISTS ${EXPRESSION}`, -}; diff --git a/libs/microorm-database/src/lib/config-cli.ts b/libs/microorm-database/src/lib/config-cli.ts index d4520b98..20385027 100644 --- a/libs/microorm-database/src/lib/config-cli.ts +++ b/libs/microorm-database/src/lib/config-cli.ts @@ -19,8 +19,8 @@ const pgSqlOptions: PgOptions = { }; const config: Options = { - // dbName: process.env['DB_NAME'], - dbName: 'microorm-test', + dbName: process.env['DB_NAME'], + // dbName: 'microorm-test', host: process.env['DB_HOST'], port: parseInt(`${process.env['DB_PORT']}`, 10), user: process.env['DB_USERNAME'], diff --git a/libs/microorm-database/src/lib/entities/users.ts b/libs/microorm-database/src/lib/entities/users.ts index c8fed805..703ac292 100644 --- a/libs/microorm-database/src/lib/entities/users.ts +++ b/libs/microorm-database/src/lib/entities/users.ts @@ -6,6 +6,7 @@ import { OneToOne, Collection, OneToMany, + ArrayType, } from '@mikro-orm/core'; import { Roles, Addresses, IAddresses, Comments, BookList } from './'; @@ -22,10 +23,11 @@ export class Users { public id!: number; @Property({ - type: 'varchar', + // type: 'varchar', length: 100, nullable: false, unique: true, + type: new ArrayType((i) => parseFloat(i)), }) public login!: string; diff --git a/package-lock.json b/package-lock.json index 09a70518..48805ff6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@mikro-orm/mysql": "^6.4.3", "@mikro-orm/nestjs": "^6.0.2", "@mikro-orm/postgresql": "^6.4.3", + "@mikro-orm/sql-highlighter": "^1.0.1", "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", @@ -58,6 +59,7 @@ "@angular/cli": "~19.0.0", "@angular/compiler-cli": "19.0.3", "@angular/language-service": "19.0.3", + "@electric-sql/pglite": "^0.2.16", "@faker-js/faker": "^8.4.1", "@jorgebodega/typeorm-factory": "^1.4.0", "@jorgebodega/typeorm-seeding": "^6.0.1", @@ -91,6 +93,7 @@ "jest-environment-node": "^29.7.0", "jest-preset-angular": "14.4.2", "jsonc-eslint-parser": "^2.1.0", + "knex-pglite": "^0.11.0", "ng-packagr": "19.0.1", "nx": "20.2.1", "pg-mem": "^3.0.2", @@ -2732,6 +2735,12 @@ "node": ">=10.0.0" } }, + "node_modules/@electric-sql/pglite": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.2.16.tgz", + "integrity": "sha512-dCSHpoOKuTxecaYhWDRp2yFTN3XWcMPMrBVl5yOR8VZEUprz4+R3iuU7BipmlsqBnBDO/6l9H/C2ZwJdunkWyw==", + "dev": true + }, "node_modules/@emnapi/core": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", @@ -4728,6 +4737,17 @@ "node": ">=12" } }, + "node_modules/@mikro-orm/sql-highlighter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@mikro-orm/sql-highlighter/-/sql-highlighter-1.0.1.tgz", + "integrity": "sha512-iO+FwRNuqNDVlIo5zfgOu2mMGVicX/FqzP+F/A0xpJLHyqvWyXzVwntgAMimBjQaxiX9Rpmc0u3Jq6/A6V6JQA==", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, "node_modules/@module-federation/bridge-react-webpack-plugin": { "version": "0.7.6", "resolved": "https://registry.npmjs.org/@module-federation/bridge-react-webpack-plugin/-/bridge-react-webpack-plugin-0.7.6.tgz", @@ -11047,7 +11067,6 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, "engines": { "node": ">=6" } @@ -18265,6 +18284,16 @@ } } }, + "node_modules/knex-pglite": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/knex-pglite/-/knex-pglite-0.11.0.tgz", + "integrity": "sha512-Z3v+vaF8C/VMJll7J2NHqFZo31ijLfYBsHMKU8jTnjfIF0edUonAB3sbAZYmUbSeJDDGfZdJPsvcB4ZyGBERcA==", + "dev": true, + "dependencies": { + "@electric-sql/pglite": "^0.2.14", + "knex": "3.1.0" + } + }, "node_modules/knex/node_modules/colorette": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", diff --git a/package.json b/package.json index 80a0c276..f2157a87 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@mikro-orm/mysql": "^6.4.3", "@mikro-orm/nestjs": "^6.0.2", "@mikro-orm/postgresql": "^6.4.3", + "@mikro-orm/sql-highlighter": "^1.0.1", "@nestjs/common": "^10.3.0", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "10.3.3", @@ -63,6 +64,7 @@ "@angular/cli": "~19.0.0", "@angular/compiler-cli": "19.0.3", "@angular/language-service": "19.0.3", + "@electric-sql/pglite": "^0.2.16", "@faker-js/faker": "^8.4.1", "@jorgebodega/typeorm-factory": "^1.4.0", "@jorgebodega/typeorm-seeding": "^6.0.1", @@ -96,6 +98,7 @@ "jest-environment-node": "^29.7.0", "jest-preset-angular": "14.4.2", "jsonc-eslint-parser": "^2.1.0", + "knex-pglite": "^0.11.0", "ng-packagr": "19.0.1", "nx": "20.2.1", "pg-mem": "^3.0.2", From 0a662598dfc7def1958f7ad39c04c526db017993 Mon Sep 17 00:00:00 2001 From: Alex H Date: Tue, 11 Feb 2025 06:19:48 +0100 Subject: [PATCH 17/26] test(json-api-nestjs): Fixe e2e test --- .e2e-micro.env | 2 + .e2e.env | 1 + .github/workflows/ci.yml | 1 + .test.env | 2 + apps/json-api-server-e2e/project.json | 11 + .../json-api/json-api-sdk/get-method.spec.ts | 7 +- .../src/lib/service/json-api-utils.service.ts | 2 +- .../mock-utils/microrom/entities/comments.ts | 2 + .../src/lib/mock-utils/microrom/index.ts | 43 +- .../lib/mock-utils/microrom/utils/init-db.ts | 3 +- .../lib/mock-utils/typeorm/entities/users.ts | 8 +- .../lib/modules/micro-orm/factory/index.ts | 6 +- .../delete-relationship.ts | 31 +- .../orm-methods/get-all/get-all.spec.ts | 72 ++-- .../micro-orm/orm-methods/get-all/get-all.ts | 23 +- .../get-all/get-query-for-count.spec.ts | 12 +- .../orm-methods/get-one/get-one.spec.ts | 8 +- .../micro-orm/orm-methods/get-one/get-one.ts | 5 +- .../get-relationship/get-relationship.ts | 3 +- .../orm-methods/patch-one/patch-one.ts | 3 +- .../service/micro-orm-util.service.ts | 29 +- .../micro-orm/service/microorm-service.ts | 18 +- .../modules/mixin/pipe/query/query.pipe.ts | 1 - .../json-api-transformer.service.spec.ts | 6 +- .../service/json-api-transformer.service.ts | 16 +- .../mixin/zod/zod-share/relationships.ts | 4 - .../src/lib/modules/type-orm/factory/index.ts | 97 ++--- .../modules/type-orm/orm-helper/index.spec.ts | 157 ++++++++ .../lib/modules/type-orm/orm-helper/index.ts | 121 +++++- .../orm-methods/delete-one/delete-one.spec.ts | 30 +- .../delete-relationship.spec.ts | 27 +- .../orm-methods/get-all/get-all.spec.ts | 44 ++- .../type-orm/orm-methods/get-all/get-all.ts | 6 +- .../orm-methods/get-one/get-one.spec.ts | 30 +- .../type-orm/orm-methods/get-one/get-one.ts | 5 +- .../get-relationship/get-relationship.spec.ts | 31 +- .../get-relationship/get-relationship.ts | 10 +- .../orm-methods/patch-one/patch-one.spec.ts | 41 +- .../orm-methods/patch-one/patch-one.ts | 18 +- .../patch-relationship.spec.ts | 26 +- .../orm-methods/post-one/post-one.spec.ts | 62 +-- .../type-orm/orm-methods/post-one/post-one.ts | 13 +- .../post-relationship.spec.ts | 26 +- .../service/entity-props-map.service.spec.ts | 85 ---- .../service/entity-props-map.service.ts | 102 ----- .../src/lib/modules/type-orm/service/index.ts | 2 - .../service/transform-data.service.spec.ts | 372 ------------------ .../service/transform-data.service.ts | 259 ------------ .../type-orm/service/type-orm.service.ts | 6 +- .../type-orm/type-orm-json-api.module.ts | 14 +- .../src/lib/entities/comments.ts | 1 + .../src/lib/entities/users.ts | 5 +- ...0250123123708_CreateUsersRolesRelations.ts | 14 +- .../src/lib/entities/users-have-roles.ts | 4 +- .../src/lib/entities/users.ts | 8 +- ...1607701632300-CreateUsersHaveRolesTable.ts | 10 +- .../1665719467563-CreateUsersHasBookTable.ts | 10 +- package-lock.json | 10 + package.json | 1 + 59 files changed, 747 insertions(+), 1219 deletions(-) create mode 100644 .e2e-micro.env create mode 100644 .test.env delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts delete mode 100644 libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts diff --git a/.e2e-micro.env b/.e2e-micro.env new file mode 100644 index 00000000..866e5ea8 --- /dev/null +++ b/.e2e-micro.env @@ -0,0 +1,2 @@ +DB_LOGGING=0 +ORM_TYPE=microorm diff --git a/.e2e.env b/.e2e.env index f48c488a..eda3206d 100644 --- a/.e2e.env +++ b/.e2e.env @@ -1 +1,2 @@ DB_LOGGING=0 +ORM_TYPE=typeorm diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c3542bb..63fda5e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,6 +85,7 @@ jobs: - run: npm run typeorm migration:run - run: npm run seed:run - run: npx nx affected -t e2e --parallel=1 + - run: npx nx affected -t e2e-micro --parallel=1 - name: Save cached .nx id: cache-dependencies-save uses: actions/cache/save@v4 diff --git a/.test.env b/.test.env new file mode 100644 index 00000000..4f233b1b --- /dev/null +++ b/.test.env @@ -0,0 +1,2 @@ +NODE_OPTIONS=--experimental-vm-modules --disable-warning=ExperimentalWarning +DB_LOGGING=0 diff --git a/apps/json-api-server-e2e/project.json b/apps/json-api-server-e2e/project.json index 94445c8f..4c159112 100644 --- a/apps/json-api-server-e2e/project.json +++ b/apps/json-api-server-e2e/project.json @@ -16,6 +16,17 @@ "passWithNoTests": true, "parallel": 1 } + }, + "e2e-micro": { + "executor": "@nx/jest:jest", + "outputs": [ + "{workspaceRoot}/coverage/{e2eProjectRoot}" + ], + "options": { + "jestConfig": "apps/json-api-server-e2e/jest.config.ts", + "passWithNoTests": true, + "parallel": 1 + } } } } diff --git a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts index cd12fe45..50bbd940 100644 --- a/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts +++ b/apps/json-api-server-e2e/src/json-api/json-api-sdk/get-method.spec.ts @@ -96,10 +96,10 @@ describe('GET method:', () => { return Promise.all(tmp); }) ); - await Promise.all(addressArray); + await Promise.all( - [...usersArray, ...commentsArray, ...rolesArray].map((i) => - jsonSdk.jonApiSdkService.deleteOne(i) + [...usersArray, ...commentsArray, ...rolesArray, ...addressArray].map( + (i) => jsonSdk.jonApiSdkService.deleteOne(i) ) ); }); @@ -339,6 +339,7 @@ describe('GET method:', () => { userItem.id, { include: ['addresses'] } ); + expect(result).toBe(`${resultGetOne.addresses.id}`); }); diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts index 68dc6f0e..93fe473d 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts @@ -21,7 +21,6 @@ import { HttpParams, isObject, isRelation, - kebabToCamel, ObjectTyped, } from '../utils'; import { ID_KEY } from '../constants'; @@ -273,6 +272,7 @@ export class JsonApiUtilsService { (includedItem) => includedItem.type === item.type && includedItem.id === item.id ); + if (!relatedIncluded) return; const entityObject = { diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts index 2b354d6d..1f42db64 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/entities/comments.ts @@ -47,6 +47,8 @@ export class Comments { updatedAt: Date = new Date(); @ManyToOne(() => Users, { + // #TODO need add chaeck for nullable relation to zod + nullable: true, fieldName: 'created_by', }) createdBy!: IUsers; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts index c481f17a..03279fd7 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts @@ -33,12 +33,12 @@ import { CURRENT_ENTITY, GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; export * from './entities'; export * from './utils'; -export const entities = [Users, UserGroups, Roles, Comments, Addresses, Notes]; - import { sharedConnect, initMikroOrm, pullAllData } from './utils'; import { DEFAULT_ARRAY_TYPE } from '../../modules/micro-orm/constants'; import { JsonApiTransformerService } from '../../modules/mixin/service/json-api-transformer.service'; +export const entities = [Users, UserGroups, Roles, Comments, Addresses, Notes]; + export function mockDbPgLiteTestModule(dbName = `test_db_${Date.now()}`) { const mikroORM = { provide: MikroORM, @@ -54,24 +54,6 @@ export function mockDbPgLiteTestModule(dbName = `test_db_${Date.now()}`) { }; } -export function mockDBTestModule(db: IMemoryDb): DynamicModule { - const mikroORM = { - provide: MikroORM, - useFactory: () => - db.adapters.createMikroOrm({ - highlighter: new SqlHighlighter(), - entities: [Users, UserGroups, Roles, Comments, Addresses, Notes], - driver: PostgreSqlDriver, - allowGlobalContext: true, - debug: ['query', 'query-params'], - }), - }; - return { - module: MikroOrmModule, - providers: [mikroORM], - exports: [mikroORM], - }; -} const readOnlyDbName = `readonly_db_${Date.now()}`; export function dbRandomName(readOnly = false) { @@ -114,27 +96,6 @@ export function getModuleForPgLite( }).compile(); } -export function getModuleFor( - db: IMemoryDb, - entity: E -): Promise { - return Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - CurrentMicroOrmProvider(), - CurrentEntityManager(), - CurrentEntityMetadata(), - CurrentEntityRepository(entity), - MicroOrmUtilService, - { - provide: CURRENT_ENTITY, - useValue: entity, - }, - OrmServiceFactory(), - ], - }).compile(); -} - export function getDefaultQuery(): Query { return { [QueryField.filter]: { diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts index e1e8a83d..a213e21c 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/utils/init-db.ts @@ -64,7 +64,8 @@ export async function initMikroOrm(knex: TypeKnex, testDbName: string) { entities: [Users, UserGroups, Roles, Comments, Addresses, Notes], allowGlobalContext: true, schema: 'public', - debug: ['query', 'query-params'], + debug: + process.env['DB_LOGGING'] !== '0' ? ['query', 'query-params'] : false, }); if ((result['rows'] as []).length === 0) { diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/users.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/users.ts index bdf61878..070ae412 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/users.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/typeorm/entities/users.ts @@ -72,20 +72,20 @@ export class Users { public isActive!: boolean; @Column({ - name: 'created_at', + name: 'test_date', type: 'timestamp', nullable: true, default: 'CURRENT_TIMESTAMP', }) - public createdAt!: Date; + public testDate!: Date; @Column({ - name: 'test_date', + name: 'created_at', type: 'timestamp', nullable: true, default: 'CURRENT_TIMESTAMP', }) - public testDate!: Date; + public createdAt!: Date; @UpdateDateColumn({ name: 'updated_at', diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts index d1e1c088..ad5a42d4 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts @@ -12,7 +12,6 @@ import { CURRENT_DATA_SOURCE_TOKEN, CURRENT_ENTITY_MANAGER_TOKEN, CURRENT_ENTITY_REPOSITORY, - FIELD_FOR_ENTITY, GLOBAL_MODULE_OPTIONS_TOKEN, RUN_IN_TRANSACTION_FUNCTION, ORM_SERVICE, @@ -21,12 +20,11 @@ import { import { EntityClass, - EntityTarget, ObjectLiteral, ResultMicroOrmModuleOptions, RunInTransaction, } from '../../../types'; -import { GetFieldForEntity, ZodEntityProps } from '../../mixin/types'; +import { ZodEntityProps } from '../../mixin/types'; import { getProps, getRelation, @@ -117,7 +115,7 @@ export function RunInTransactionFactory(): FactoryProvider { provide: RUN_IN_TRANSACTION_FUNCTION, inject: [], useFactory() { - return async (callback) => {}; + return async (callback) => callback(); }, }; } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts index ca1f34a1..881b1d27 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts @@ -1,9 +1,10 @@ import { EntityRelation } from '@klerick/json-api-nestjs-shared'; -import { ObjectLiteral } from '../../../../types'; +import { ObjectLiteral, ValidateQueryError } from '../../../../types'; import { PostRelationshipData } from '../../../mixin/zod'; import { MicroOrmService } from '../../service'; +import { NotFoundException } from '@nestjs/common'; export async function deleteRelationship< E extends ObjectLiteral, @@ -19,10 +20,30 @@ export async function deleteRelationship< input ); - const currentEntityRef = this.microOrmUtilService.entityManager.getReference( - this.microOrmUtilService.entity, - id as any - ); + // const currentEntityRef = this.microOrmUtilService.entityManager.getReference( + // this.microOrmUtilService.entity, + // id as any + // ); + + const currentEntityRef = await this.microOrmUtilService + .queryBuilder() + .where({ + [this.microOrmUtilService.currentPrimaryColumn]: id, + }) + .getSingleResult(); + + if (!currentEntityRef) { + const error: ValidateQueryError = { + code: 'invalid_arguments', + message: `Resource '${this.microOrmUtilService.currentAlias}' with id '${id}' does not exist`, + path: ['fields'], + }; + throw new NotFoundException([error]); + } + + await this.microOrmUtilService.entityManager.populate(currentEntityRef, [ + rel as any, + ]); if (Array.isArray(idsResult)) { const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts index 6ad530ed..9a823b3f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.spec.ts @@ -49,10 +49,7 @@ describe('get-all', () => { const queryBuilder = microOrmServiceUser.microOrmUtilService.queryBuilder(); - const checkData = await queryBuilder - .clone() - .limit(1) - .execute('all', true); + const checkData = await queryBuilder.clone().limit(1).getResult(); const count = await queryBuilder.clone().count(); const query = getDefaultQuery(); @@ -71,10 +68,7 @@ describe('get-all', () => { const queryBuilder = microOrmServiceUser.microOrmUtilService.queryBuilder(); - const checkData = await queryBuilder - .clone() - .limit(5, 5) - .execute('all', true); + const checkData = await queryBuilder.clone().limit(5, 5).getResult(); const count = await queryBuilder.clone().count(); const query = getDefaultQuery(); query.page = { @@ -102,7 +96,7 @@ describe('get-all', () => { lastName: 'DESC', }) .limit(5, 5) - .execute('all', true); + .getResult(); const count = await queryBuilder.clone().count(); const query = getDefaultQuery(); @@ -142,7 +136,7 @@ describe('get-all', () => { }, }) .limit(5, 5) - .execute('all', true); + .getResult(); const count = await queryBuilder.clone().count(); @@ -185,7 +179,7 @@ describe('get-all', () => { id: 'ASC', }) .limit(5, 5) - .execute('all', true); + .getResult(); const count = await queryBuilder .clone() @@ -219,10 +213,7 @@ describe('get-all', () => { it('default', async () => { const queryBuilder = microOrmServiceUser.microOrmUtilService.queryBuilder(); - const checkData = await queryBuilder - .clone() - .limit(5) - .execute('all', true); + const checkData = await queryBuilder.clone().limit(5).getResult(); const count = await queryBuilder.clone().count(); const query = getDefaultQuery(); query.page = { @@ -251,7 +242,7 @@ describe('get-all', () => { .clone() .select(['id', ...select]) .limit(5) - .execute('all', true); + .getResult(); const count = await queryBuilder.clone().count(); const query = getDefaultQuery(); query.page = { @@ -285,10 +276,7 @@ describe('get-all', () => { 'state', ]) .leftJoinAndSelect('Users.roles', 'Roles__roles', {}, ['name', 'key']); - const checkData = await queryBuilder - .clone() - .limit(5) - .execute('all', true); + const checkData = await queryBuilder.clone().limit(5).getResult(); const count = await queryBuilder.clone().count().distinct(); const query = getDefaultQuery(); query.page = { @@ -317,10 +305,7 @@ describe('get-all', () => { .queryBuilder('Users') .leftJoinAndSelect('Users.roles', 'Roles__roles'); - const checkData = await queryBuilder - .clone() - .limit(5) - .execute('all', true); + const checkData = await queryBuilder.clone().limit(5).getResult(); const count = await queryBuilder.clone().count().distinct(); const query = getDefaultQuery(); @@ -349,10 +334,7 @@ describe('get-all', () => { 'state', ]); - const checkData = await queryBuilder - .clone() - .limit(5) - .execute('all', true); + const checkData = await queryBuilder.clone().limit(5).getResult(); const count = await queryBuilder.clone().count().distinct(); const query = getDefaultQuery(); @@ -384,20 +366,20 @@ describe('get-all', () => { beforeAll(async () => { rolesData = await microOrmServiceUser.microOrmUtilService .queryBuilder(Roles) - .execute('all', true); + .getResult(); addresses = await microOrmServiceUser.microOrmUtilService .queryBuilder(Addresses) - .execute('all', true); + .getResult(); userGroups = await microOrmServiceUser.microOrmUtilService .queryBuilder(UserGroups) - .execute('all', true); + .getResult(); users = await microOrmServiceUser.microOrmUtilService .queryBuilder() - .execute('all', true); + .getResult(); notes = await microOrmServiceUser.microOrmUtilService .queryBuilder(Notes) - .execute('all', true); + .getResult(); }); describe('target', () => { @@ -417,7 +399,7 @@ describe('get-all', () => { .orderBy({ login: 'DESC', }) - .execute('all', true); + .getResult(); const count = await queryBuilder.clone().count(); const query = getDefaultQuery(); @@ -464,7 +446,7 @@ describe('get-all', () => { login: 'DESC', }, }) - .execute('all', true); + .getResult(); const count = await queryBuilder.clone().count(); const queryBuilder2 = microOrmServiceUser.microOrmUtilService @@ -483,7 +465,7 @@ describe('get-all', () => { city: 'DESC', }, }) - .execute('all', true); + .getResult(); const count2 = await queryBuilder2.clone().count(); const query = getDefaultQuery(); @@ -555,7 +537,7 @@ describe('get-all', () => { .orderBy({ login: 'DESC', }) - .execute('all', true); + .getResult(); const count = await queryBuilder.clone().count(); const query = getDefaultQuery(); @@ -593,7 +575,7 @@ describe('get-all', () => { label: 'DESC', }, }) - .execute('all', true); + .getResult(); const count2 = await queryBuilder.clone().count(); @@ -639,7 +621,7 @@ describe('get-all', () => { id: 'DESC', }, }) - .execute('all', true); + .getResult(); const count3 = await queryBuilder2.clone().count().distinct(); @@ -673,7 +655,7 @@ describe('get-all', () => { .clone() .limit(5) .orderBy({ id: 'DESC' }) - .execute('all', true); + .getResult(); const count4 = await queryBuilder2.clone().count().distinct(); const query4 = getDefaultQuery(); @@ -719,7 +701,7 @@ describe('get-all', () => { login: 'DESC', }, }) - .execute('all', true); + .getResult(); const count = await queryBuilder.clone().count(); @@ -765,7 +747,7 @@ describe('get-all', () => { key: 'DESC', }, }) - .execute('all', true); + .getResult(); const count1 = await queryBuilder1.clone().count().distinct(); @@ -820,7 +802,7 @@ describe('get-all', () => { city: 'DESC', }, }) - .execute('all', true); + .getResult(); const count = await queryBuilder.clone().count(); @@ -880,7 +862,7 @@ describe('get-all', () => { city: 'DESC', }, }) - .execute('all', true); + .getResult(); const count = await quweryBuilder.clone().count(); const query = getDefaultQuery(); query.page = { @@ -935,7 +917,7 @@ describe('get-all', () => { city: 'DESC', }, }) - .execute('all', true); + .getResult(); const count = await queryBuilder.clone().count(); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts index 7a06f141..e33a71e5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-all.ts @@ -1,4 +1,4 @@ -import { QueryFlag } from '@mikro-orm/core'; +import { QueryFlag, serialize, wrap } from '@mikro-orm/core'; import { ObjectLiteral } from '../../../../types'; import { MicroOrmService } from '../../service'; @@ -73,18 +73,19 @@ export async function getAll( }); const sortObject = getSortObject(query); + const resultList = await this.microOrmUtilService + .prePareQueryBuilder(resultQueryBuilder, query) + .orderBy( + Object.keys(sortObject).length > 0 + ? sortObject + : { + [this.microOrmUtilService.currentPrimaryColumn]: 'ASC', + } + ) + .getResult(); return { totalItems: count, - items: await this.microOrmUtilService - .prePareQueryBuilder(resultQueryBuilder, query) - .orderBy( - Object.keys(sortObject).length > 0 - ? sortObject - : { - [this.microOrmUtilService.currentPrimaryColumn]: 'ASC', - } - ) - .execute('all', true), + items: resultList.map((i) => wrap(i).toJSON() as E), }; } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts index 1c434aad..8192be1d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts @@ -68,7 +68,7 @@ describe('get-query-for-count', () => { >(microOrmServiceUser, ...[query]); expect(result.getFormattedQuery()).toBe( - `select "Users"."id" from "public"."users" as "Users" left join "public"."users_have_roles" as "u2" on "Users"."id" = "u2"."user_id" left join "public"."roles" as "u1" on "u2"."role_id" = "u1"."id" order by "Users"."id" asc, "Users"."last_name" desc, "Users"."user_groups_id" asc, "u1"."name" desc` + `select "Users".* from "public"."users" as "Users" left join "public"."users_have_roles" as "u2" on "Users"."id" = "u2"."user_id" left join "public"."roles" as "u1" on "u2"."role_id" = "u1"."id" order by "Users"."id" asc, "Users"."last_name" desc, "Users"."user_groups_id" asc, "u1"."name" desc` ); }); @@ -170,7 +170,7 @@ describe('get-query-for-count', () => { >(microOrmServiceUser, ...[query]); expect(result.getFormattedQuery()).toBe( - `select "Users"."id" from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and "Users"."test_real" && '{test}'` + `select "Users".* from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and "Users"."test_real" && '{test}' order by "Users"."id" asc` ); const result1 = getQueryForCount.call< @@ -180,7 +180,7 @@ describe('get-query-for-count', () => { >(microOrmServiceUser, ...[query1]); expect(result1.getFormattedQuery()).toBe( - `select "Users"."id" from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and (exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id" and "Comments"."kind" = 'COMMENT')) and (exists (select 1 from "public"."user_groups" as "UserGroups" where "UserGroups"."id" = "Users"."user_groups_id" and "UserGroups"."label" = 'test'))` + `select "Users".* from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and (exists (select 1 from "public"."comments" as "Comments" where "Comments"."created_by" = "Users"."id" and "Comments"."kind" = 'COMMENT')) and (exists (select 1 from "public"."user_groups" as "UserGroups" where "UserGroups"."id" = "Users"."user_groups_id" and "UserGroups"."label" = 'test')) order by "Users"."id" asc` ); const result2 = getQueryForCount.call< @@ -189,7 +189,7 @@ describe('get-query-for-count', () => { ReturnType> >(microOrmServiceUser, ...[query2]); expect(result2.getFormattedQuery()).toBe( - `select "Users"."id" from "public"."users" as "Users" left join "public"."users" as "u1" on "Users"."manager_id" = "u1"."id" left join "public"."addresses" as "a2" on "Users"."addresses_id" = "a2"."id" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and "u1"."login" = 'test' and "a2"."city" = 'test'` + `select "Users".* from "public"."users" as "Users" left join "public"."users" as "u1" on "Users"."manager_id" = "u1"."id" left join "public"."addresses" as "a2" on "Users"."addresses_id" = "a2"."id" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and "u1"."login" = 'test' and "a2"."city" = 'test' order by "Users"."id" asc` ); const result3 = getQueryForCount.call< @@ -198,7 +198,7 @@ describe('get-query-for-count', () => { ReturnType> >(microOrmServiceUser, ...[query3]); expect(result3.getFormattedQuery()).toBe( - `select "Users"."id" from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and (exists (select 1 from "public"."users_have_roles" as "users_have_roles" left join "public"."roles" as "r1" on "users_have_roles"."role_id" = "r1"."id" where "users_have_roles"."user_id" = "Users"."id" and "r1"."key" = 'test' and "r1"."key" != 'test2' and "r1"."is_default" = 'false'))` + `select "Users".* from "public"."users" as "Users" where "Users"."login" = 'test' and "Users"."login" != 'test2' and "Users"."is_active" = 'false' and (exists (select 1 from "public"."users_have_roles" as "users_have_roles" left join "public"."roles" as "r1" on "users_have_roles"."role_id" = "r1"."id" where "users_have_roles"."user_id" = "Users"."id" and "r1"."key" = 'test' and "r1"."key" != 'test2' and "r1"."is_default" = 'false')) order by "Users"."id" asc` ); }); @@ -236,7 +236,7 @@ describe('get-query-for-count', () => { ReturnType> >(microOrmServiceUser, ...[query1]); expect(result1.getFormattedQuery()).toBe( - `select "Users"."id" from "public"."users" as "Users" left join "public"."users_have_roles" as "u2" on "Users"."id" = "u2"."user_id" left join "public"."roles" as "u1" on "u2"."role_id" = "u1"."id" where "u1"."key" = 'test' and "u1"."key" != 'test2' and "u1"."is_default" = 'false' order by "Users"."id" asc, "Users"."last_name" desc, "Users"."user_groups_id" asc, "u1"."name" desc` + `select "Users".* from "public"."users" as "Users" left join "public"."users_have_roles" as "u2" on "Users"."id" = "u2"."user_id" left join "public"."roles" as "u1" on "u2"."role_id" = "u1"."id" where "u1"."key" = 'test' and "u1"."key" != 'test2' and "u1"."is_default" = 'false' order by "Users"."id" asc, "Users"."last_name" desc, "Users"."user_groups_id" asc, "u1"."name" desc` ); }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts index df09dce8..e6fcaf70 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.spec.ts @@ -44,9 +44,9 @@ describe('get-one', () => { const checkData = await microOrmServiceUser.microOrmUtilService .queryBuilder() .limit(1) - .execute('get', true); + .getSingleResult(); const query = getDefaultQuery(); - + if (!checkData) throw new Error('Result is null'); const result = await getOne.call< MicroOrmService, Parameters>, @@ -69,8 +69,8 @@ describe('get-one', () => { }, }) .limit(1) - .execute('get', true); - + .getSingleResult(); + if (!checkData) throw new Error('Result is null'); const query = getDefaultQuery(); query.include = ['addresses', 'comments', 'manager']; query.fields = { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts index 124baa51..3e01eecb 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-one/get-one.ts @@ -2,6 +2,7 @@ import { NotFoundException } from '@nestjs/common'; import { ObjectLiteral, ValidateQueryError } from '../../../../types'; import { QueryOne } from '../../../mixin/zod'; import { MicroOrmService } from '../../service'; +import { serialize, wrap } from '@mikro-orm/core'; export async function getOne( this: MicroOrmService, @@ -14,7 +15,7 @@ export async function getOne( const resultItem = await this.microOrmUtilService .prePareQueryBuilder(queryBuilder, query) - .execute('get', true); + .getSingleResult(); if (!resultItem) { const error: ValidateQueryError = { @@ -25,5 +26,5 @@ export async function getOne( throw new NotFoundException([error]); } - return resultItem; + return wrap(resultItem).toJSON() as E; } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts index 70bb7644..dd752173 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts @@ -3,6 +3,7 @@ import { NotFoundException } from '@nestjs/common'; import { MicroOrmService } from '../../service'; import { ObjectLiteral, ValidateQueryError } from '../../../../types'; +import { serialize } from '@mikro-orm/core'; export async function getRelationship< E extends ObjectLiteral, @@ -30,5 +31,5 @@ export async function getRelationship< throw new NotFoundException([error]); } - return result; + return serialize(result, { forceObject: true }) as unknown as E; } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts index 44bd5bca..dc7590cd 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts @@ -43,10 +43,11 @@ export async function patchOne( if (attributes) { const attrTarget = this.microOrmUtilService.createEntity(attributes as any); + for (const [props, val] of ObjectTyped.entries(attrTarget)) { + if (!(props in attributes)) continue; existEntity[props] = val; } } - return this.microOrmUtilService.saveEntity(existEntity, relationships); } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts index 10b0a56b..dc7ff359 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts @@ -80,6 +80,7 @@ export class MicroOrmUtilService { private _relationsName!: EntityKey[]; private _relationMap = new Map, EntityProperty>(); + private _relationPropsMap = new Map, EntityKey[]>(); get relationsName() { if (!this._relationsName) { this._relationsName = this.metadata.relations.map((r) => r.name); @@ -88,6 +89,21 @@ export class MicroOrmUtilService { return this._relationsName; } + getRelationProps(entity: EntityClass) { + const props = this._relationPropsMap.get(entity); + if (props) { + return props; + } + const relMetaData = this.entityManager.getMetadata(entity); + const relation = relMetaData.relations.map((i) => i.name); + const newProps = relMetaData.props + .filter((r) => !relation.includes(r.name)) + .map((r) => r.name); + this._relationPropsMap.set(entity, newProps); + + return newProps; + } + getRelation(name: EntityKey) { let relation = this._relationMap.get(name); if (!relation) { @@ -418,7 +434,7 @@ export class MicroOrmUtilService { item ); - let selectJoin: undefined | string[] = undefined; + let selectJoin: string[] = this.getRelationProps(relationEntity); if (item in relationFields) { const tmpSet = new Set([ ...relationFields[item], @@ -595,7 +611,16 @@ export class MicroOrmUtilService { for await (const item of this.asyncIterateFindRelationships( relationships )) { - Object.assign(targetInstance, item); + const itemProps = ObjectTyped.entries(item).at(0); + if (!itemProps) continue; + const [nameProps, data] = itemProps; + + if ((targetInstance[nameProps] as any) instanceof Collection) { + targetInstance[nameProps].removeAll(); + targetInstance[nameProps].add(...(data as [])); + } else { + Object.assign(targetInstance, item); + } } } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts index 367396c8..8c2d28a7 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts @@ -103,8 +103,14 @@ export class MicroOrmService implements OrmService { [QueryField.include]: Object.keys(relationships || {}), } as any; + const resultForResponse = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, result[this.microOrmUtilService.currentPrimaryColumn], fakeQuery); + const { data, included } = this.jsonApiTransformerService.transformData( - result, + resultForResponse, fakeQuery ); @@ -118,7 +124,7 @@ export class MicroOrmService implements OrmService { id: number | string, inputData: PatchData ): Promise> { - const result = await patchOne.call< + await patchOne.call< MicroOrmService, Parameters>, ReturnType> @@ -130,8 +136,14 @@ export class MicroOrmService implements OrmService { [QueryField.include]: Object.keys(relationships || {}), } as any; + const resultForResponse = await getOne.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, fakeQuery); + const { data, included } = this.jsonApiTransformerService.transformData( - result, + resultForResponse, fakeQuery ); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts index 293687bb..d4c5d359 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.ts @@ -18,7 +18,6 @@ export class QueryPipe transform(value: InputQuery): Query { try { - console.log(JSON.stringify(value)); return this.zodQuerySchema.parse(value); } catch (e) { if (e instanceof ZodError) { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts index 089ad7c5..3b9444f7 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.spec.ts @@ -211,7 +211,7 @@ describe('JsonApiTransformerService - extractAttributes', () => { }; if (i === 'roles') { acum[i]['data'] = userObject.roles.map((relName) => ({ - id: relName.id, + id: relName.id.toString(), type: mapProps.get(Roles)?.typeName, })); } @@ -233,7 +233,7 @@ describe('JsonApiTransformerService - extractAttributes', () => { }; if (i === 'addresses') { acum[i]['data'] = { - id: userObject.addresses.id, + id: userObject.addresses.id.toString(), type: mapProps.get(Addresses)?.typeName, }; } @@ -633,7 +633,7 @@ describe('JsonApiTransformerService - extractAttributes', () => { relationships: { addresses: { data: { - id: userObject.addresses.id, + id: userObject.addresses.id.toString(), type: mapProps.get(Addresses)?.typeName, }, links: { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts index 6cff4a9b..974473b5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts @@ -160,13 +160,15 @@ export class JsonApiTransformerService { if (Array.isArray(props)) { return props.map((i: any) => ({ type: relationMapPops.typeName, - id: i[relationMapPops.primaryColumnName], + id: i[relationMapPops.primaryColumnName].toString(), })); } else { - return { - type: relationMapPops.typeName, - id: props.primaryColumnName, - } as any; + return props + ? ({ + type: relationMapPops.typeName, + id: props[relationMapPops.primaryColumnName].toString(), + } as any) + : null; } } @@ -205,7 +207,7 @@ export class JsonApiTransformerService { // @ts-expect-error incorrect parse acum[i as keyof Relationships]['data'] = item[i].map( (rel: any) => ({ - id: rel[relationMapPops.primaryColumnName], + id: rel[relationMapPops.primaryColumnName].toString(), type: relationMapPops.typeName, }) ); @@ -217,7 +219,7 @@ export class JsonApiTransformerService { // @ts-expect-error incorrect parse acum[i as keyof Relationships]['data'] = item[i] ? { - id: item[i][relationMapPops.primaryColumnName], + id: item[i][relationMapPops.primaryColumnName].toString(), type: relationMapPops.typeName, } : null; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts index 8963091f..425253b8 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts @@ -15,7 +15,6 @@ import { } from '../../types'; import { zodRelData } from './rel-data'; import { nonEmptyObject } from '../zod-utils'; -import { Users } from '../../../../mock-utils/microrom'; function getZodRuleForData< K extends string, @@ -156,6 +155,3 @@ export type Relationships< T extends ObjectLiteral, K extends true | false = false > = z.infer>; - -const tmp = {} as Relationships; -const r = tmp.roles; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts index 5dfc69ae..0c365cb1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts @@ -1,27 +1,23 @@ import { FactoryProvider } from '@nestjs/common'; import { getDataSourceToken } from '@nestjs/typeorm'; -import { camelToKebab, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { camelToKebab } from '@klerick/json-api-nestjs-shared'; import { DataSource, EntityManager, Repository } from 'typeorm'; import { CURRENT_ENTITY_MANAGER_TOKEN, FIND_ONE_ROW_ENTITY, CHECK_RELATION_NAME, - PARAMS_FOR_ZOD_SCHEMA, ORM_SERVICE, - FIELD_FOR_ENTITY, RUN_IN_TRANSACTION_FUNCTION, GLOBAL_MODULE_OPTIONS_TOKEN, CURRENT_DATA_SOURCE_TOKEN, CURRENT_ENTITY_REPOSITORY, + ENTITY_MAP_PROPS, } from '../../../constants'; import { - EntityProps, - FieldWithType, FindOneRowEntity, CheckRelationNme, - ZodParams, - GetFieldForEntity, + ZodEntityProps, } from '../../mixin/types'; import { ObjectLiteral, @@ -30,25 +26,24 @@ import { RequiredFromPartial, ConfigParam, RunInTransaction, + EntityClass, } from '../../../types'; -import { - getField, - getPropsTreeForRepository, - getArrayPropsForEntity, - getTypeForAllProps, - getRelationTypeArray, - getTypePrimaryColumn, - getFieldWithType, - getPropsFromDb, - getRelationTypeName, - getRelationTypePrimaryColumn, -} from '../orm-helper'; import { TypeOrmService, TypeormUtilsService } from '../service'; import { getEntityName } from '../../mixin/helper'; import { TypeOrmJsonApiModule } from '../type-orm-json-api.module'; import { TypeOrmParam } from '../type'; +import { + getProps, + getRelation, + getPropsType, + getPropsNullable, + getPrimaryColumnName, + getPrimaryColumnType, + getRelationProperty, +} from '../orm-helper'; + export function CurrentDataSourceProvider( connectionName?: string ): FactoryProvider { @@ -78,52 +73,32 @@ export function CurrentEntityRepository( }; } -export function GetFieldForEntity(): FactoryProvider< - GetFieldForEntity -> { +export function EntityPropsMap( + entities: EntityClass[] +) { return { - provide: FIELD_FOR_ENTITY, - useFactory: (entityManager: EntityManager) => { - return (entity: EntityTarget) => - getField(entityManager.getRepository(entity)); - }, + provide: ENTITY_MAP_PROPS, inject: [CURRENT_ENTITY_MANAGER_TOKEN], - }; -} + useFactory: (entityManager: EntityManager) => { + const mapProperty = new Map, ZodEntityProps>(); -export function ZodParamsFactory(): FactoryProvider< - ZodParams> -> { - return { - provide: PARAMS_FOR_ZOD_SCHEMA, - inject: [CURRENT_ENTITY_REPOSITORY], - useFactory: (repo: Repository) => { - const primaryColumns = repo.metadata.primaryColumns[0] - .propertyName as EntityProps; - const fieldWithType = ObjectTyped.entries(getFieldWithType(repo)) - .filter(([key]) => key !== repo.metadata.primaryColumns[0].propertyName) - .reduce( - (acum, [key, type]) => ({ - ...acum, - [key]: type, - }), - {} as FieldWithType - ); + for (const item of entities) { + const entityRepo = entityManager.getRepository(item); - return { - entityFieldsStructure: getField(repo), - entityRelationStructure: getPropsTreeForRepository(repo), - propsArray: getArrayPropsForEntity(repo), - propsType: getTypeForAllProps(repo), - typeId: getTypePrimaryColumn(repo), - typeName: camelToKebab(getEntityName(repo.target)), - fieldWithType, - propsDb: getPropsFromDb(repo), - primaryColumn: primaryColumns, - relationArrayProps: getRelationTypeArray(repo), - relationPopsName: getRelationTypeName(repo), - primaryColumnType: getRelationTypePrimaryColumn(repo), - } satisfies ZodParams>; + const className = getEntityName(item); + mapProperty.set(item, { + props: getProps(entityRepo), + propsType: getPropsType(entityRepo), + propsNullable: getPropsNullable(entityRepo), + primaryColumnName: getPrimaryColumnName(entityRepo), + primaryColumnType: getPrimaryColumnType(entityRepo), + typeName: camelToKebab(className), + className: className, + relations: getRelation(entityRepo), + relationProperty: getRelationProperty(entityRepo), + }); + } + return mapProperty; }, }; } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts index f462b1f0..35cb3fae 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts @@ -263,3 +263,160 @@ describe('type-orm-helper', () => { }); }); }); + +import { + getProps, + getRelation, + getPropsType, + getPropsNullable, + getPrimaryColumnName, + getPrimaryColumnType, + getRelationProperty, +} from './'; + +describe('typeorm-orm-helper-for-map', () => { + let userRepository: Repository; + let addressesRepository: Repository; + let notesRepository: Repository; + let commentsRepository: Repository; + let rolesRepository: Repository; + let userGroupRepository: Repository; + let db: IMemoryDb; + let user: Users; + let userWithRelation: Users; + beforeAll(async () => { + db = createAndPullSchemaBase(); + const module: TestingModule = await Test.createTestingModule({ + imports: [mockDBTestModule(db)], + providers: [...providerEntities(getDataSourceToken())], + }).compile(); + + ({ + userRepository, + addressesRepository, + notesRepository, + commentsRepository, + rolesRepository, + userGroupRepository, + } = getRepository(module)); + }); + + it('getProps', () => { + const result = getProps(userRepository); + expect(result.includes('id')).toBe(true); + expect(result.includes('lastName')).toBe(true); + expect(result.includes('createdAt')).toBe(true); + expect(result.includes('updatedAt')).toBe(true); + expect(result.includes('isActive')).toBe(true); + expect(result.includes('login')).toBe(true); + expect(result.includes('firstName')).toBe(true); + expect(result.includes('testReal')).toBe(true); + expect(result.includes('testArrayNull')).toBe(true); + expect(result.includes('testDate')).toBe(true); + + expect(result.includes('userGroup' as any)).toBe(false); + expect(result.includes('notes' as any)).toBe(false); + expect(result.includes('comments' as any)).toBe(false); + expect(result.includes('roles' as any)).toBe(false); + expect(result.includes('manager' as any)).toBe(false); + expect(result.includes('addresses' as any)).toBe(false); + }); + + it('getPropsType', () => { + const result = getPropsType(userRepository); + + expect(result).toEqual({ + createdAt: 'date', + firstName: 'string', + id: 'number', + isActive: 'boolean', + lastName: 'string', + login: 'string', + testArrayNull: 'array', + testDate: 'date', + testReal: 'array', + updatedAt: 'date', + }); + }); + + it('getPropsNullable', () => { + const result = getPropsNullable(userRepository); + expect(result).toEqual([ + 'firstName', + 'testReal', + 'testArrayNull', + 'lastName', + 'isActive', + 'testDate', + 'createdAt', + 'updatedAt', + ]); + }); + + it('getPrimaryColumnName', () => { + const result = getPrimaryColumnName(userRepository); + expect(result).toBe('id'); + }); + + it('getPrimaryColumnType', () => { + const result = getPrimaryColumnType(userRepository); + expect(result).toBe(TypeField.number); + }); + + it('getRelation', () => { + const result = getRelation(userRepository); + expect(result.includes('id' as any)).toBe(false); + expect(result.includes('lastName' as any)).toBe(false); + expect(result.includes('createdAt' as any)).toBe(false); + expect(result.includes('updatedAt' as any)).toBe(false); + expect(result.includes('isActive' as any)).toBe(false); + expect(result.includes('login' as any)).toBe(false); + expect(result.includes('firstName' as any)).toBe(false); + expect(result.includes('testReal' as any)).toBe(false); + expect(result.includes('testArrayNull' as any)).toBe(false); + expect(result.includes('testDate' as any)).toBe(false); + + expect(result.includes('userGroup')).toBe(true); + expect(result.includes('notes')).toBe(true); + expect(result.includes('comments')).toBe(true); + expect(result.includes('roles')).toBe(true); + expect(result.includes('manager')).toBe(true); + expect(result.includes('addresses')).toBe(true); + }); + + it('getRelationProperty', () => { + const result = getRelationProperty(userRepository); + expect(result).toEqual({ + addresses: { + entityClass: Addresses, + isArray: false, + nullable: true, + }, + comments: { + entityClass: Comments, + isArray: true, + nullable: false, + }, + manager: { + entityClass: Users, + isArray: false, + nullable: true, + }, + notes: { + entityClass: Notes, + isArray: true, + nullable: false, + }, + roles: { + entityClass: Roles, + isArray: true, + nullable: false, + }, + userGroup: { + entityClass: UserGroups, + isArray: false, + nullable: true, + }, + }); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts index 613fd01a..a96321e9 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts @@ -5,7 +5,7 @@ import { } from '@klerick/json-api-nestjs-shared'; import { Repository } from 'typeorm'; -import { ObjectLiteral } from '../../../types'; +import { ObjectLiteral, ResultMicroOrmModuleOptions } from '../../../types'; import { RelationTree, ValueOf, @@ -28,8 +28,13 @@ import { ColumnType, RelationPropsTypeName, RelationPrimaryColumnType, + TupleOfEntityRelation, + TupleOfEntityProps, + FilterNullableProps, + RelationProperty, } from '../../mixin/types'; import { getEntityName } from '../../mixin/helper'; +import { EntityMetadata } from '@mikro-orm/core'; export type ConcatFieldWithRelation< R extends string, @@ -296,3 +301,117 @@ export const getRelationTypePrimaryColumn = ( return acum; }, {} as Record) as RelationPrimaryColumnType; }; +// ----- + +export const getRelation = ( + repository: Repository +) => + repository.metadata.relations.map((i) => { + return i.propertyName; + }) as TupleOfEntityRelation; + +export const getProps = ( + repository: Repository +): TupleOfEntityProps => { + const relations = getRelation(repository); + + return repository.metadata.columns + .filter((i) => !relations.includes(i.propertyName)) + .map((r) => r.propertyName) as TupleOfEntityProps; +}; + +export const getPropsType = ( + repository: Repository +): FieldWithType => { + const field = getProps(repository); + + const entity = repository.target as any; + const result = {} as any; + for (const item of field) { + let typeProps: TypeField = TypeField.string; + + const fieldMetadata = repository.metadata.columns.find( + (i) => i.propertyName === item + ); + + if (fieldMetadata?.isArray) { + result[item] = TypeField.array; + continue; + } + + switch (Reflect.getMetadata('design:type', entity['prototype'], item)) { + case Array: + typeProps = TypeField.array; + break; + case Date: + typeProps = TypeField.date; + break; + case Number: + typeProps = TypeField.number; + break; + case Boolean: + typeProps = TypeField.boolean; + break; + case Object: + typeProps = TypeField.object; + break; + default: + typeProps = TypeField.string; + } + + result[item] = fieldMetadata?.isArray ? TypeField.array : typeProps; + } + + return result; +}; + +export const getPropsNullable = ( + repository: Repository +): FilterNullableProps> => { + const relation = getRelation(repository); + return repository.metadata.columns + .filter((i) => !relation.includes(i.propertyName)) + .map((i) => + i.isNullable || i.default !== undefined ? i.propertyName : false + ) + .filter((i) => !!i) as FilterNullableProps>; +}; + +export const getPrimaryColumnName = ( + repository: Repository +) => { + const column = repository.metadata.primaryColumns.at(0); + if (!column) throw new Error('Primary column not found'); + + return column.propertyName; +}; + +export const getPrimaryColumnType = ( + repository: Repository +): TypeForId => { + const target = repository.target as any; + const primaryColumn = repository.metadata.primaryColumns[0].propertyName; + + return Reflect.getMetadata( + 'design:type', + target['prototype'], + primaryColumn + ) === Number + ? TypeField.number + : TypeField.string; +}; + +export const getRelationProperty = ( + repository: Repository +): RelationProperty => { + return repository.metadata.relations.reduce((acum, item) => { + // @ts-expect-error its dynamic creater + acum[item.propertyName] = { + entityClass: item.inverseEntityMetadata.target, + nullable: item.isManyToMany || item.isOneToMany ? false : item.isNullable, + isArray: item.isManyToMany || item.isOneToMany, + }; + + return acum; + }, {} as RelationProperty); +}; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts index 818aaa5d..c47d008e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-one/delete-one.spec.ts @@ -12,25 +12,25 @@ import { CurrentEntityRepository, OrmServiceFactory, } from '../../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../../constants'; +import { CURRENT_ENTITY, DEFAULT_CONNECTION_NAME } from '../../../../constants'; -import { getRepository, pullUser, Users } from '../../../../mock-utils/typeorm'; +import { + getRepository, + pullUser, + Users, + entities, +} from '../../../../mock-utils/typeorm'; import { Repository } from 'typeorm'; import { CONTROL_OPTIONS_TOKEN, ORM_SERVICE } from '../../../../constants'; - -import { - EntityPropsMapService, - TypeOrmService, - TransformDataService, - TypeormUtilsService, -} from '../../service'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { EntityPropsMap } from '../../factory'; describe('deleteOne', () => { let db: IMemoryDb; let typeormService: TypeOrmService; - let transformDataService: TransformDataService; let user: Users; let userRepository: Repository; @@ -42,6 +42,10 @@ describe('deleteOne', () => { providers: [ ...providerEntities(getDataSourceToken()), CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), + { + provide: CURRENT_ENTITY, + useValue: Users, + }, { provide: CONTROL_OPTIONS_TOKEN, useValue: { @@ -49,19 +53,17 @@ describe('deleteOne', () => { debug: false, }, }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); ({ userRepository } = getRepository(module)); user = await pullUser(userRepository); typeormService = module.get>(ORM_SERVICE); - transformDataService = - module.get>(TransformDataService); }); it('Should be ok', async () => { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts index 4f729db2..627f0917 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.spec.ts @@ -6,6 +6,7 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, + entities, getRepository, mockDBTestModule, Notes, @@ -18,6 +19,7 @@ import { import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, ORM_SERVICE, } from '../../../../constants'; @@ -25,21 +27,17 @@ import { CurrentDataSourceProvider, CurrentEntityManager, CurrentEntityRepository, + EntityPropsMap, OrmServiceFactory, } from '../../factory'; - -import { - EntityPropsMapService, - TypeOrmService, - TransformDataService, - TypeormUtilsService, -} from '../../service'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; import { createAndPullSchemaBase } from '../../../../mock-utils'; describe('deleteRelationship', () => { let db: IMemoryDb; let typeormService: TypeOrmService; - let transformDataService: TransformDataService; + let transformDataService: JsonApiTransformerService; let typeormUtilsService: TypeormUtilsService; let userRepository: Repository; let addressesRepository: Repository; @@ -62,12 +60,16 @@ describe('deleteRelationship', () => { debug: false, }, }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); ({ @@ -87,8 +89,9 @@ describe('deleteRelationship', () => { userGroupRepository ); typeormService = module.get>(ORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + transformDataService = module.get>( + JsonApiTransformerService + ); typeormUtilsService = module.get>(TypeormUtilsService); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts index 4efaebde..a55b95c2 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts @@ -1,12 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getDataSourceToken } from '@nestjs/typeorm'; import { QueryField } from '@klerick/json-api-nestjs-shared'; -import { Equal, IsNull, Repository } from 'typeorm'; +import { Equal, Repository } from 'typeorm'; import { IMemoryDb } from 'pg-mem'; import { Addresses, Comments, + entities, getRepository, mockDBTestModule, Notes, @@ -20,6 +21,7 @@ import { CurrentDataSourceProvider, CurrentEntityManager, CurrentEntityRepository, + EntityPropsMap, OrmServiceFactory, } from '../../factory'; import { @@ -28,18 +30,15 @@ import { ORM_SERVICE, DEFAULT_QUERY_PAGE, DEFAULT_PAGE_SIZE, + CURRENT_ENTITY, } from '../../../../constants'; import { ObjectLiteral as Entity } from '../../../../types'; import { Query } from '../../../mixin/zod'; -import { - EntityPropsMapService, - TypeOrmService, - TransformDataService, - TypeormUtilsService, -} from '../../service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; function getDefaultQuery() { const filter = { @@ -63,7 +62,7 @@ function getDefaultQuery() { describe('getAll', () => { let db: IMemoryDb; let typeormService: TypeOrmService; - let transformDataService: TransformDataService; + let transformDataService: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -86,12 +85,16 @@ describe('getAll', () => { debug: false, }, }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); ({ @@ -111,8 +114,9 @@ describe('getAll', () => { userGroupRepository ); typeormService = module.get>(ORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + transformDataService = module.get>( + JsonApiTransformerService + ); }); afterEach(() => { @@ -150,7 +154,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith(checkData); + expect(spyOnTransformData).toBeCalledWith(checkData, query); }); it('include', async () => { @@ -177,7 +181,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); it('select', async () => { @@ -221,7 +225,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); describe('filter', () => { @@ -271,7 +275,7 @@ describe('getAll', () => { id: { eq: `${checkData?.id}` }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); it('Check relation with the same Entity', async () => { @@ -299,7 +303,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); // it('Target relation is null', async () => { @@ -341,7 +345,7 @@ describe('getAll', () => { }; query.include = ['manager']; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); it('Relation one-to-many', async () => { @@ -369,7 +373,7 @@ describe('getAll', () => { }, }; await typeormService.getAll(query); - expect(spyOnTransformData).toBeCalledWith([checkData]); + expect(spyOnTransformData).toBeCalledWith([checkData], query); }); it('Relation many-to-many', async () => { @@ -399,7 +403,7 @@ describe('getAll', () => { }, }; const { data } = await typeormService.getAll(query); - expect(spyOnTransformData).not.toBeCalled(); + expect(spyOnTransformData).not.toHaveBeenCalled(); expect(data).toEqual([]); }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts index 2fe92c5e..8e098e76 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts @@ -250,8 +250,10 @@ export async function getAll( ); } const resultData = await resultQuery.getMany(); - const { included, data } = - this.transformDataService.transformData(resultData); + const { included, data } = this.transformDataService.transformData( + resultData, + query + ); return { meta: { pageNumber: page.number, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts index 90dec8a8..53c9d874 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts @@ -8,6 +8,7 @@ import { ObjectLiteral as Entity } from '../../../../types'; import { Addresses, Comments, + entities, getRepository, mockDBTestModule, Notes, @@ -20,6 +21,7 @@ import { import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, DEFAULT_PAGE_SIZE, DEFAULT_QUERY_PAGE, @@ -29,16 +31,13 @@ import { CurrentDataSourceProvider, CurrentEntityManager, CurrentEntityRepository, + EntityPropsMap, OrmServiceFactory, } from '../../factory'; import { Query } from '../../../mixin/zod'; import { NotFoundException } from '@nestjs/common'; -import { - EntityPropsMapService, - TypeOrmService, - TransformDataService, - TypeormUtilsService, -} from '../../service'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; import { createAndPullSchemaBase } from '../../../../mock-utils'; function getDefaultQuery() { @@ -62,7 +61,7 @@ function getDefaultQuery() { describe('getOne', () => { let db: IMemoryDb; let typeormService: TypeOrmService; - let transformDataService: TransformDataService; + let transformDataService: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -85,12 +84,16 @@ describe('getOne', () => { debug: false, }, }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); ({ @@ -110,8 +113,9 @@ describe('getOne', () => { userGroupRepository ); typeormService = module.get>(ORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + transformDataService = module.get>( + JsonApiTransformerService + ); }); afterEach(() => { @@ -136,7 +140,7 @@ describe('getOne', () => { }); query.include = ['addresses', 'comments']; await typeormService.getOne('1', query); - expect(spyOnTransformData).toBeCalledWith(checkData); + expect(spyOnTransformData).toBeCalledWith(checkData, query); }); it('Get one item with select', async () => { const spyOnTransformData = jest.spyOn( @@ -177,7 +181,7 @@ describe('getOne', () => { manager: ['login'], }; await typeormService.getOne('1', query); - expect(spyOnTransformData).toBeCalledWith(checkData); + expect(spyOnTransformData).toBeCalledWith(checkData, query); }); it('Should be error', async () => { expect.assertions(1); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts index 257ef7cb..42e58c61 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts @@ -86,7 +86,10 @@ export async function getOne( }; throw new NotFoundException([error]); } - const { included, data } = this.transformDataService.transformData(result); + const { included, data } = this.transformDataService.transformData( + result, + query + ); return { meta: {}, data, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts index 11dd1bdc..979323d1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.spec.ts @@ -6,6 +6,7 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, + entities, getRepository, mockDBTestModule, Notes, @@ -18,6 +19,7 @@ import { import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, ORM_SERVICE, } from '../../../../constants'; @@ -25,22 +27,19 @@ import { CurrentDataSourceProvider, CurrentEntityManager, CurrentEntityRepository, + EntityPropsMap, OrmServiceFactory, } from '../../factory'; import { NotFoundException } from '@nestjs/common'; -import { - EntityPropsMapService, - TypeOrmService, - TransformDataService, - TypeormUtilsService, -} from '../../service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('getRelationship', () => { let db: IMemoryDb; let typeormService: TypeOrmService; - let transformDataService: TransformDataService; + let transformDataService: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -63,12 +62,16 @@ describe('getRelationship', () => { debug: false, }, }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); ({ @@ -88,8 +91,9 @@ describe('getRelationship', () => { userGroupRepository ); typeormService = module.get>(ORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + transformDataService = module.get>( + JsonApiTransformerService + ); }); afterEach(() => { @@ -98,10 +102,7 @@ describe('getRelationship', () => { }); it('Should be ok', async () => { - const spyOnTransformData = jest.spyOn( - transformDataService, - 'getRelationships' - ); + const spyOnTransformData = jest.spyOn(transformDataService, 'transformRel'); const id = 1; const rel = 'roles'; const check = await userRepository.findOne({ diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts index d50f45a9..5a76cc87 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts @@ -54,16 +54,8 @@ export async function getRelationship< }; throw new NotFoundException([error]); } + const data = this.transformDataService.transformRel(result, rel); - const { data } = this.transformDataService.getRelationships(result, rel); - if (data === undefined) { - const error: ValidateQueryError = { - code: 'custom', - message: `transformDataService.getRelationships return undefined`, - path: [], - }; - throw new InternalServerErrorException([error]); - } return { meta: {}, data, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts index 30944561..7e8ca456 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.spec.ts @@ -6,6 +6,7 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, + entities, getRepository, mockDBTestModule, Notes, @@ -19,28 +20,26 @@ import { CurrentDataSourceProvider, CurrentEntityManager, CurrentEntityRepository, + EntityPropsMap, OrmServiceFactory, } from '../../factory'; import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, ORM_SERVICE, } from '../../../../constants'; import { PatchData, PostData } from '../../../mixin/zod'; -import { - EntityPropsMapService, - TypeOrmService, - TransformDataService, - TypeormUtilsService, -} from '../../service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('patchOne', () => { let db: IMemoryDb; let backaUp: IBackup; let typeormService: TypeOrmService; - let transformDataService: TransformDataService; + let transformDataService: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -77,12 +76,16 @@ describe('patchOne', () => { debug: false, }, }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); @@ -104,8 +107,9 @@ describe('patchOne', () => { ); typeormService = module.get>(ORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + transformDataService = module.get>( + JsonApiTransformerService + ); notes = await notesRepository.find(); users = await userRepository.find(); @@ -232,7 +236,10 @@ describe('patchOne', () => { const result = await userRepository.findOneBy({ id: parseInt(withoutRelationships.id as string, 10), }); - expect(spyOnTransformData).toBeCalledWith(result); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: [], + }); expect(returnData).not.toHaveProperty('included'); }); @@ -262,7 +269,10 @@ describe('patchOne', () => { }, }); - expect(spyOnTransformData).toBeCalledWith(result); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: ['addresses', 'notes', 'roles', 'manager', 'userGroup'], + }); expect(returnData).toHaveProperty('included'); }); @@ -302,7 +312,10 @@ describe('patchOne', () => { }, }); - expect(spyOnTransformData).toBeCalledWith(result); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: ['addresses', 'notes', 'roles', 'manager', 'userGroup'], + }); expect(returnData).toHaveProperty('included'); }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts index 90b57273..70c613cf 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts @@ -3,10 +3,14 @@ import { UnprocessableEntityException, } from '@nestjs/common'; import { DeepPartial } from 'typeorm'; -import { ResourceObject, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { + ResourceObject, + ObjectTyped, + QueryField, +} from '@klerick/json-api-nestjs-shared'; import { ObjectLiteral, ValidateQueryError } from '../../../../types'; -import { PatchData } from '../../../mixin/zod'; +import { PatchData, Query } from '../../../mixin/zod'; import { TypeOrmService } from '../../service'; export async function patchOne( @@ -63,7 +67,15 @@ export async function patchOne( relationships ); - const { data, included } = this.transformDataService.transformData(saveData); + const fakeQuery: Query = { + [QueryField.fields]: null, + [QueryField.include]: Object.keys(relationships || {}), + } as any; + + const { data, included } = this.transformDataService.transformData( + saveData, + fakeQuery + ); const includeData = included ? { included } : {}; return { meta: {}, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts index d44ed6dc..4e3c326a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.spec.ts @@ -6,6 +6,7 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, + entities, getRepository, mockDBTestModule, Notes, @@ -18,6 +19,7 @@ import { import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, ORM_SERVICE, } from '../../../../constants'; @@ -25,20 +27,17 @@ import { CurrentDataSourceProvider, CurrentEntityManager, CurrentEntityRepository, + EntityPropsMap, OrmServiceFactory, } from '../../factory'; -import { - EntityPropsMapService, - TypeOrmService, - TransformDataService, - TypeormUtilsService, -} from '../../service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('patchRelationship', () => { let db: IMemoryDb; let typeormService: TypeOrmService; - let transformDataService: TransformDataService; + let transformDataService: JsonApiTransformerService; let typeormUtilsService: TypeormUtilsService; let userRepository: Repository; let addressesRepository: Repository; @@ -61,12 +60,16 @@ describe('patchRelationship', () => { debug: false, }, }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); ({ @@ -86,8 +89,9 @@ describe('patchRelationship', () => { userGroupRepository ); typeormService = module.get>(ORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + transformDataService = module.get>( + JsonApiTransformerService + ); typeormUtilsService = module.get>(TypeormUtilsService); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts index 8e7b223d..e3deab69 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.spec.ts @@ -6,6 +6,7 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, + entities, getRepository, mockDBTestModule, Notes, @@ -18,6 +19,7 @@ import { } from '../../../../mock-utils/typeorm'; import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, ORM_SERVICE, } from '../../../../constants'; @@ -27,25 +29,22 @@ import { CurrentDataSourceProvider, CurrentEntityManager, CurrentEntityRepository, + EntityPropsMap, OrmServiceFactory, } from '../../factory'; -import { - EntityPropsMapService, - TypeOrmService, - TransformDataService, - TypeormUtilsService, -} from '../../service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('postOne', () => { let db: IMemoryDb; let backaUp: IBackup; let typeormService: TypeOrmService; - let transformDataService: TransformDataService; + let transformDataService: JsonApiTransformerService; let podsRepository: Repository; let typeormServicePods: TypeOrmService; - let transformDataServicePods: TransformDataService; + let transformDataServicePods: JsonApiTransformerService; let userRepository: Repository; let addressesRepository: Repository; @@ -80,12 +79,16 @@ describe('postOne', () => { debug: false, }, }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); @@ -101,12 +104,16 @@ describe('postOne', () => { debug: false, }, }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Pods), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); @@ -129,12 +136,14 @@ describe('postOne', () => { ); backaUp = db.backup(); typeormService = module.get>(ORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + transformDataService = module.get>( + JsonApiTransformerService + ); typeormServicePods = modulePods.get>(ORM_SERVICE); - transformDataServicePods = - modulePods.get>(TransformDataService); + transformDataServicePods = modulePods.get>( + JsonApiTransformerService + ); notes = await notesRepository.find(); users = await userRepository.find(); @@ -207,10 +216,13 @@ describe('postOne', () => { id, }); - expect(spyOnTransformData).toBeCalledWith({ - ...result, - id, - }); + expect(spyOnTransformData).toBeCalledWith( + { + ...result, + id, + }, + { fields: null, include: [] } + ); expect(returnData).not.toHaveProperty('included'); }); @@ -226,7 +238,10 @@ describe('postOne', () => { login, }); - expect(spyOnTransformData).toBeCalledWith(result); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: [], + }); expect(returnData).not.toHaveProperty('included'); }); @@ -250,7 +265,10 @@ describe('postOne', () => { }, }); - expect(spyOnTransformData).toBeCalledWith(result); + expect(spyOnTransformData).toBeCalledWith(result, { + fields: null, + include: ['notes', 'roles', 'manager', 'userGroup'], + }); expect(returnData).toHaveProperty('included'); }); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts index d4decd3b..4e85e139 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts @@ -1,7 +1,7 @@ import { DeepPartial } from 'typeorm'; -import { ResourceObject } from '@klerick/json-api-nestjs-shared'; +import { QueryField, ResourceObject } from '@klerick/json-api-nestjs-shared'; import { ObjectLiteral } from '../../../../types'; -import { PostData } from '../../../mixin/zod'; +import { PostData, Query } from '../../../mixin/zod'; import { TypeOrmService } from '../../service'; export async function postOne( @@ -28,8 +28,15 @@ export async function postOne( entityTarget, relationships ); + const fakeQuery: Query = { + [QueryField.fields]: null, + [QueryField.include]: Object.keys(relationships || {}), + } as any; - const { data, included } = this.transformDataService.transformData(saveData); + const { data, included } = this.transformDataService.transformData( + saveData, + fakeQuery + ); const includeData = included ? { included } : {}; return { meta: {}, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts index 70a647e5..5a936228 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.spec.ts @@ -6,6 +6,7 @@ import { Repository } from 'typeorm'; import { Addresses, Comments, + entities, getRepository, mockDBTestModule, Notes, @@ -18,6 +19,7 @@ import { import { CONTROL_OPTIONS_TOKEN, + CURRENT_ENTITY, DEFAULT_CONNECTION_NAME, ORM_SERVICE, } from '../../../../constants'; @@ -25,20 +27,17 @@ import { CurrentDataSourceProvider, CurrentEntityManager, CurrentEntityRepository, + EntityPropsMap, OrmServiceFactory, } from '../../factory'; -import { - EntityPropsMapService, - TypeOrmService, - TransformDataService, - TypeormUtilsService, -} from '../../service'; +import { TypeOrmService, TypeormUtilsService } from '../../service'; import { createAndPullSchemaBase } from '../../../../mock-utils'; +import { JsonApiTransformerService } from '../../../mixin/service/json-api-transformer.service'; describe('postRelationship', () => { let db: IMemoryDb; let typeormService: TypeOrmService; - let transformDataService: TransformDataService; + let transformDataService: JsonApiTransformerService; let typeormUtilsService: TypeormUtilsService; let userRepository: Repository; let addressesRepository: Repository; @@ -61,12 +60,16 @@ describe('postRelationship', () => { debug: false, }, }, + { + provide: CURRENT_ENTITY, + useValue: Users, + }, + EntityPropsMap(entities as any), CurrentEntityManager(), CurrentEntityRepository(Users), TypeormUtilsService, - TransformDataService, + JsonApiTransformerService, OrmServiceFactory(), - EntityPropsMapService, ], }).compile(); ({ @@ -86,8 +89,9 @@ describe('postRelationship', () => { userGroupRepository ); typeormService = module.get>(ORM_SERVICE); - transformDataService = - module.get>(TransformDataService); + transformDataService = module.get>( + JsonApiTransformerService + ); typeormUtilsService = module.get>(TypeormUtilsService); }); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts deleted file mode 100644 index d62de08e..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.spec.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; - -import { - mockDBTestModule, - providerEntities, - UserGroups, - Users, -} from '../../../mock-utils/typeorm'; -import { CurrentDataSourceProvider } from '../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../constants'; -import { EntityPropsMapService } from './entity-props-map.service'; -import { createAndPullSchemaBase } from '../../../mock-utils'; - -describe('EntityPropsMapService', () => { - let db: IMemoryDb; - let entityPropsMapService: EntityPropsMapService; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - EntityPropsMapService, - ], - }).compile(); - - entityPropsMapService = module.get( - EntityPropsMapService - ); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('getPropsForEntity', () => { - const result = entityPropsMapService.getRelPropsForEntity(Users); - expect(result).toEqual([ - 'addresses', - 'manager', - 'roles', - 'comments', - 'notes', - 'userGroup', - ]); - }); - - it('getPropsForEntity', () => { - const result = entityPropsMapService.getPropsForEntity(Users); - expect(result).toEqual([ - 'id', - 'login', - 'firstName', - 'testReal', - 'testArrayNull', - 'lastName', - 'isActive', - 'createdAt', - 'testDate', - 'updatedAt', - ]); - }); - - it('getPrimaryColumnsForEntity', () => { - expect(entityPropsMapService.getPrimaryColumnsForEntity(Users)).toBe('id'); - }); - - it('getNameForEntity', () => { - expect(entityPropsMapService.getNameForEntity(Users)).toBe('Users'); - expect(entityPropsMapService.getNameForEntity(UserGroups)).toBe( - 'UserGroups' - ); - }); - - it('getRelationPropsType', () => { - expect( - entityPropsMapService.getRelationPropsType(Users, 'userGroup' as any) - ).toEqual(UserGroups); - }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts deleted file mode 100644 index db0455f5..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/entity-props-map.service.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { DataSource, EntityTarget } from 'typeorm'; -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; - -import { ObjectLiteral as Entity } from '../../../types'; -import { - ResultGetField, - TupleOfEntityProps, - TupleOfEntityRelation, -} from '../../mixin/types'; -import { getField } from '../orm-helper'; -import { CURRENT_DATA_SOURCE_TOKEN } from '../../../constants'; - -@Injectable() -export class EntityPropsMapService { - @Inject(CURRENT_DATA_SOURCE_TOKEN) private dataSource!: DataSource; - - private _propsForEntity: Map = new Map(); - private _relPropsForEntity: Map = new Map(); - private _relTypePropsForEntity: Map = new Map(); - private _primaryColumnsForEntity: Map = new Map(); - private _nameForEntity: Map = new Map(); - - getPropsForEntity(entity: E): TupleOfEntityProps { - const result = this._propsForEntity.get(entity); - if (result) { - return result; - } - - const { field } = this.pullPropsAndRelFoEntity(entity); - - return field; - } - - getRelPropsForEntity(entity: E): TupleOfEntityRelation { - const result = this._relPropsForEntity.get(entity); - if (result) { - return result; - } - - const { relations } = this.pullPropsAndRelFoEntity(entity); - - return relations; - } - - getRelationPropsType(entity: E, rel: EntityRelation) { - const result = this._relTypePropsForEntity.get(entity); - if (result) { - return result[rel]; - } - - const repo = this.dataSource.getRepository( - entity as unknown as EntityTarget - ); - const relToType = repo.metadata.relations.reduce((acum, item) => { - acum[item.propertyName as keyof E] = item.inverseEntityMetadata.target; - return acum; - }, {} as Record); - - this._relTypePropsForEntity.set(entity, relToType); - return relToType[rel]; - } - - getPrimaryColumnsForEntity(entity: E): keyof E { - const result = this._primaryColumnsForEntity.get(entity); - if (result) { - return result as keyof E; - } - const primaryColumns = this.dataSource.getRepository( - entity as unknown as EntityTarget - ).metadata.primaryColumns[0].propertyName; - this._primaryColumnsForEntity.set(entity, primaryColumns); - - return primaryColumns as keyof E; - } - - getNameForEntity(entity: E): string { - const result = this._nameForEntity.get(entity); - if (result) { - return result; - } - const name = this.dataSource.getRepository( - entity as unknown as EntityTarget - ).metadata.name; - this._nameForEntity.set(entity, name); - return name; - } - - private pullPropsAndRelFoEntity( - entity: E - ): ResultGetField { - const repo = this.dataSource.getRepository( - entity as unknown as EntityTarget - ); - - const { relations, field } = getField(repo); - this._propsForEntity.set(entity, field); - this._relPropsForEntity.set(entity, relations); - - return { relations, field }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts index 2a3590c8..b8e93ec7 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/index.ts @@ -1,4 +1,2 @@ export * from './type-orm.service'; -export * from './transform-data.service'; export * from './typeorm-utils.service'; -export * from './entity-props-map.service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts deleted file mode 100644 index ce1b9871..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.spec.ts +++ /dev/null @@ -1,372 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { IMemoryDb } from 'pg-mem'; -import { getDataSourceToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; - -import { - Addresses, - Comments, - getRepository, - mockDBTestModule, - Notes, - providerEntities, - pullAllData, - Roles, - UserGroups, - Users, -} from '../../../mock-utils/typeorm'; -import { CurrentDataSourceProvider, CurrentEntityRepository } from '../factory'; -import { DEFAULT_CONNECTION_NAME } from '../../../constants'; -import { TransformDataService } from './transform-data.service'; -import { ApplicationConfig } from '@nestjs/core'; -import { VersioningType } from '@nestjs/common'; -import { EntityPropsMapService } from '../service'; -import { createAndPullSchemaBase } from '../../../mock-utils'; - -describe('TransformDataService', () => { - let db: IMemoryDb; - let transformDataService: TransformDataService; - let userRepository: Repository; - let addressesRepository: Repository; - let notesRepository: Repository; - let commentsRepository: Repository; - let rolesRepository: Repository; - let userGroupRepository: Repository; - let applicationConfig: ApplicationConfig; - let entityPropsMapService: EntityPropsMapService; - let usersData: Users; - - beforeAll(async () => { - db = createAndPullSchemaBase(); - const module: TestingModule = await Test.createTestingModule({ - imports: [mockDBTestModule(db)], - providers: [ - ...providerEntities(getDataSourceToken()), - CurrentDataSourceProvider(DEFAULT_CONNECTION_NAME), - // TransformDataService, - // EntityPropsMapService, - ], - }).compile(); - - // ({ - // userRepository, - // addressesRepository, - // notesRepository, - // commentsRepository, - // rolesRepository, - // userGroupRepository, - // } = getRepository(module)); - // await pullAllData( - // userRepository, - // addressesRepository, - // notesRepository, - // commentsRepository, - // rolesRepository, - // userGroupRepository - // ); - - // transformDataService = - // module.get>(TransformDataService); - // entityPropsMapService = module.get( - // EntityPropsMapService - // ); - // - // const data = await userRepository.findOne({ - // where: { - // id: 1, - // }, - // relations: { - // userGroup: true, - // manager: true, - // roles: true, - // addresses: true, - // comments: true, - // notes: true, - // }, - // }); - // if (!data) { - // throw new Error('Not found user'); - // } - // usersData = data; - // applicationConfig = module.get(ApplicationConfig); - }); - - beforeEach(() => { - // transformDataService = new TransformDataService(); - // Object.defineProperty(transformDataService, 'applicationConfig', { - // value: applicationConfig, - // }); - // Object.defineProperty(transformDataService, 'entityPropsMapService', { - // value: entityPropsMapService, - // }); - }); - - afterEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - - it('Test', () => {}); - - // it('Url path', () => { - // const prefix = 'api'; - // applicationConfig.setGlobalPrefix(prefix); - // - // expect(transformDataService.urlPath).toEqual(['', prefix]); - // }); - // it('Url path with version', () => { - // const prefix = 'api'; - // const version = '1'; - // - // applicationConfig.setGlobalPrefix(prefix); - // - // jest - // .spyOn(applicationConfig, 'getVersioning') - // .mockImplementationOnce(() => ({ - // type: VersioningType.URI, - // defaultVersion: version, - // })); - // - // expect(transformDataService.urlPath).toEqual(['', prefix, 'v1']); - // }); - // - // it('check type, id and links', () => { - // const result = transformDataService.transformData(usersData); - // expect(result).toHaveProperty('data'); - // const { data } = result; - // expect(data.type).toBe('users'); - // expect(data.id).toBe(usersData.id.toString()); - // expect(data.links.self).toBe( - // [...transformDataService.urlPath, 'users', usersData.id.toString()].join( - // '/' - // ) - // ); - // }); - // - // it('check attributes', async () => { - // const result = transformDataService.transformData(usersData); - // const { - // id, - // addresses, - // comments, - // manager, - // roles, - // notes, - // userGroup, - // ...other - // } = usersData; - // expect(result.data.attributes).toEqual(other); - // const result1 = transformDataService.transformData([]); - // expect(result1.data).toEqual([]); - // - // const data = await userRepository.findOne({ - // select: { - // id: true, - // login: true, - // isActive: true, - // testDate: true, - // }, - // where: { - // id: 1, - // }, - // }); - // if (!data) { - // throw new Error('Not found user'); - // } - // const result2 = transformDataService.transformData(data); - // const { id: id2, ...other2 } = data; - // - // expect(result2.data.attributes).toEqual(other2); - // }); - // - // it('check relationships', async () => { - // const { - // data: { relationships }, - // } = transformDataService.transformData(usersData); - // expect(relationships?.addresses?.data).toEqual({ - // type: 'addresses', - // id: usersData.addresses.id.toString(), - // }); - // expect(relationships?.addresses?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/addresses` - // ); - // expect(relationships?.manager?.data).toEqual({ - // type: 'users', - // id: usersData.manager.id.toString(), - // }); - // expect(relationships?.manager?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/manager` - // ); - // expect(relationships?.roles?.data).toEqual( - // usersData.roles.map((i) => ({ - // type: 'roles', - // id: i.id.toString(), - // })) - // ); - // expect(relationships?.roles?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/roles` - // ); - // expect(relationships?.comments?.data).toEqual( - // usersData.comments.map((i) => ({ - // type: 'comments', - // id: i.id.toString(), - // })) - // ); - // expect(relationships?.comments?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/comments` - // ); - // expect(relationships?.notes?.data).toEqual( - // usersData.notes.map((i) => ({ - // type: 'notes', - // id: i.id.toString(), - // })) - // ); - // expect(relationships?.notes?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/notes` - // ); - // expect(relationships?.userGroup?.data).toEqual({ - // type: 'user-groups', - // id: usersData.userGroup.id.toString(), - // }); - // expect(relationships?.userGroup?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/userGroup` - // ); - // }); - // - // it('check relationships again', async () => { - // const data = await userRepository.findOne({ - // where: { - // id: 1, - // }, - // relations: { - // userGroup: true, - // roles: true, - // }, - // }); - // if (!data) { - // throw new Error('Not found user'); - // } - // - // const { - // data: { relationships }, - // } = transformDataService.transformData(data); - // - // expect(relationships?.addresses).not.toHaveProperty('data'); - // expect(relationships?.addresses?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/addresses` - // ); - // expect(relationships?.manager).not.toHaveProperty('data'); - // expect(relationships?.manager?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/manager` - // ); - // expect(relationships?.roles?.data).toEqual( - // usersData.roles.map((i) => ({ - // type: 'roles', - // id: i.id.toString(), - // })) - // ); - // expect(relationships?.roles?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/roles` - // ); - // expect(relationships?.comments).not.toHaveProperty('data'); - // expect(relationships?.comments?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/comments` - // ); - // expect(relationships?.notes).not.toHaveProperty('data'); - // expect(relationships?.notes?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/notes` - // ); - // expect(relationships?.userGroup?.data).toEqual({ - // type: 'user-groups', - // id: usersData.userGroup.id.toString(), - // }); - // expect(relationships?.userGroup?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/userGroup` - // ); - // data.userGroup = null as any; - // data.roles = []; - // const { - // data: { relationships: relationships2 }, - // } = transformDataService.transformData(data); - // - // expect(relationships2?.addresses).not.toHaveProperty('data'); - // expect(relationships2?.addresses?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/addresses` - // ); - // expect(relationships2?.manager).not.toHaveProperty('data'); - // expect(relationships2?.manager?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/manager` - // ); - // - // expect(relationships2?.roles?.data).toEqual([]); - // expect(relationships2?.roles?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/roles` - // ); - // expect(relationships2?.comments).not.toHaveProperty('data'); - // expect(relationships2?.comments?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/comments` - // ); - // expect(relationships2?.notes).not.toHaveProperty('data'); - // expect(relationships2?.notes?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/notes` - // ); - // expect(relationships2?.userGroup?.data).toEqual(null); - // expect(relationships2?.userGroup?.links.self).toBe( - // `${transformDataService.urlPath.join('/')}/users/${ - // usersData.id - // }/relationships/userGroup` - // ); - // }); - // - // it('check include', async () => { - // const data = await userRepository.findOne({ - // where: { - // id: 1, - // }, - // relations: { - // userGroup: true, - // roles: true, - // }, - // }); - // if (!data) { - // throw new Error('Not found user'); - // } - // - // const { included } = transformDataService.transformData(data); - // expect(included).not.toBe(undefined); - // }); -}); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts deleted file mode 100644 index b6ac2c93..00000000 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/transform-data.service.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { Inject, Injectable, VersioningType } from '@nestjs/common'; -import { ApplicationConfig } from '@nestjs/core'; -import { - Attributes, - Data, - MainData, - Relationships, - ResourceData, - ResourceObject, - camelToKebab, - ObjectTyped, - EntityRelation, -} from '@klerick/json-api-nestjs-shared'; - -import { RoutePathFactory } from '@nestjs/core/router/route-path-factory'; -import { EntityPropsMapService } from './entity-props-map.service'; -import { ObjectLiteral } from '../../../types'; - -Injectable(); -export class TransformDataService { - @Inject(ApplicationConfig) private applicationConfig!: ApplicationConfig; - @Inject(EntityPropsMapService) - private entityPropsMapService!: EntityPropsMapService; - - private _urlPath!: string[]; - - get urlPath() { - if (this._urlPath) return [...this._urlPath]; - this._urlPath = ['']; - const prefix = this.applicationConfig.getGlobalPrefix(); - const version = this.applicationConfig.getVersioning(); - - const routePathFactory = new RoutePathFactory(this.applicationConfig); - - if (prefix) { - this._urlPath.push(this.applicationConfig.getGlobalPrefix()); - } - if (version && version.type === VersioningType.URI) { - const firstVersion = Array.isArray(version.defaultVersion) - ? version.defaultVersion[0] - : version.defaultVersion; - if (firstVersion) { - this._urlPath.push( - `${routePathFactory.getVersionPrefix( - version - )}${firstVersion.toString()}` - ); - } - } - return [...this._urlPath]; - } - private getLink(...partOfUrl: string[]) { - const urlPath = this.urlPath; - urlPath.push(...partOfUrl); - return urlPath.join('/'); - } - - public transformData(data: E): Pick, 'data' | 'included'>; - public transformData( - data: E[] - ): Pick, 'data' | 'included'>; - public transformData( - data: E | E[] - ): - | Pick, 'data' | 'included'> - | Pick, 'data' | 'included'> { - const dataResponse = Array.isArray(data) - ? data - .map((i) => this.getMainData(i)) - .filter((i: ResourceData | null): i is ResourceData => !!i) - : this.getMainData(data); - - let relationships: Relationships | undefined = undefined; - if (Array.isArray(dataResponse) && dataResponse.length > 0) { - relationships = dataResponse.reduce( - (acum, item) => ({ - ...acum, - ...item.relationships, - }), - {} as Relationships - ); - } - - if (!Array.isArray(dataResponse) && dataResponse !== null) { - relationships = dataResponse.relationships; - } - - if (relationships === undefined) { - return { data: dataResponse } as Pick< - ResourceObject, - 'data' | 'included' - >; - } - - const propsNeedForInclude = ObjectTyped.entries(relationships).reduce( - (acum, [key, val]) => { - if (!val || !('data' in val)) { - return acum; - } - if (Array.isArray(val.data)) { - acum.push(key); - return acum; - } - if (!Array.isArray(val.data)) { - acum.push(key); - } - return acum; - }, - [] as EntityRelation[] - ); - - const included = (Array.isArray(data) ? data : [data]) - .reduce((acum, item) => { - const tmp = propsNeedForInclude.reduce((a, props) => { - const currentItem = item[props]; - type CurrentItem = typeof currentItem; - const r = ( - Array.isArray(currentItem) ? currentItem : [currentItem] - ) as CurrentItem[]; - - a.push(...r); - return a; - }, [] as E[EntityRelation][]); - acum.push(...tmp); - return acum; - }, [] as E[EntityRelation][]) - .map((i) => { - return this.getMainData(i as E); - }) - .filter((i) => !!i); - - if (included.length > 0) { - return { data: dataResponse, included } as unknown as Pick< - ResourceObject, - 'data' | 'included' - >; - } - return { data: dataResponse } as unknown as Pick< - ResourceObject, - 'data' | 'included' - >; - } - - getMainData(data: E): ResourceData | null { - if (!data) return null; - const entity = data.constructor as unknown as E; - return { - ...this.getData( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[this.entityPropsMapService.getPrimaryColumnsForEntity(entity)] as - | string - | number - ), - attributes: ObjectTyped.entries(data).reduce((acum, [key, val]) => { - if ( - this.entityPropsMapService - .getRelPropsForEntity(entity) - .includes(key.toString()) || - key.toString() === - this.entityPropsMapService.getPrimaryColumnsForEntity(entity) - ) { - return acum; - } - acum[key] = val; - return acum; - }, {} as Record) as Attributes, - relationships: this.transformRelationshipsData(data), - links: { - self: this.getLink( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[ - this.entityPropsMapService.getPrimaryColumnsForEntity(entity) - ] as string - ), - }, - }; - } - - getRelationships>( - data: E, - rel: Rel - ): Data { - const entity = data.constructor as unknown as E; - const relation = data[rel]; - const target = this.entityPropsMapService.getRelationPropsType(entity, rel); - const typeName = camelToKebab( - this.entityPropsMapService.getNameForEntity(target) - ) as Rel; - - const primaryColumn = - this.entityPropsMapService.getPrimaryColumnsForEntity(target); - - const resultData = Array.isArray(relation) - ? (relation as E[Rel][]).map((i: E[Rel]) => - this.getData( - typeName, - i[primaryColumn as keyof E[Rel]] as string - ) - ) - : relation === null || !relation - ? null - : this.getData( - typeName, - relation[primaryColumn as keyof E[Rel]] as string - ); - - return { - data: resultData, - } as Data; - } - - transformRelationshipsData(data: E): Relationships { - const entity = data.constructor as unknown as E; - return this.entityPropsMapService - .getRelPropsForEntity(entity) - .reduce((acum: any, val: any) => { - const result: { - links: Record; - data?: Data], EntityRelation>['data']; - } = { - links: { - self: this.getLink( - camelToKebab(this.entityPropsMapService.getNameForEntity(entity)), - data[ - this.entityPropsMapService.getPrimaryColumnsForEntity( - entity - ) as keyof E - ] as string, - 'relationships', - val - ), - }, - }; - - if (val in data) { - const { data: dataRelationships } = this.getRelationships( - data, - val as EntityRelation - ); - if (Array.isArray(dataRelationships)) { - result['data'] = dataRelationships; - } - - if (!Array.isArray(dataRelationships)) { - result['data'] = dataRelationships; - } - } - acum[val.toString()] = result; - return acum; - }, {} as Record) as Relationships; - } - - getData(type: T, id: number | string): MainData { - return { - type, - id: id.toString(), - }; - } -} diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts index 37b91c3e..b17d6c20 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts @@ -30,7 +30,7 @@ import { } from '../orm-methods'; import { TypeormUtilsService } from './typeorm-utils.service'; -import { TransformDataService } from './transform-data.service'; +import { JsonApiTransformerService } from '../../mixin/service/json-api-transformer.service'; import { CONTROL_OPTIONS_TOKEN, CURRENT_ENTITY_REPOSITORY, @@ -40,8 +40,8 @@ import { TypeOrmParam } from '../type'; export class TypeOrmService implements OrmService { @Inject(TypeormUtilsService) public typeormUtilsService!: TypeormUtilsService; - @Inject(TransformDataService) - public transformDataService!: TransformDataService; + @Inject(JsonApiTransformerService) + public transformDataService!: JsonApiTransformerService; @Inject(CONTROL_OPTIONS_TOKEN) public config!: ConfigParam & TypeOrmParam; @Inject(CURRENT_ENTITY_REPOSITORY) public repository!: Repository; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts index 5413a6d1..968e57a1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type-orm-json-api.module.ts @@ -6,19 +6,14 @@ import { NestProvider, ObjectLiteral, ResultModuleOptions } from '../../types'; import { CurrentEntityManager, CurrentDataSourceProvider, - ZodParamsFactory, CurrentEntityRepository, FindOneRowEntityFactory, CheckRelationNameFactory, OrmServiceFactory, - GetFieldForEntity, RunInTransactionFactory, + EntityPropsMap, } from './factory'; -import { - EntityPropsMapService, - TransformDataService, - TypeormUtilsService, -} from './service'; +import { TypeormUtilsService } from './service'; import { GLOBAL_MODULE_OPTIONS_TOKEN } from '../../constants'; export class TypeOrmJsonApiModule { @@ -39,8 +34,7 @@ export class TypeOrmJsonApiModule { optionProvider, CurrentDataSourceProvider(options.connectionName), CurrentEntityManager(), - GetFieldForEntity(), - EntityPropsMapService, + EntityPropsMap(options.entities), RunInTransactionFactory(), ]; @@ -57,9 +51,7 @@ export class TypeOrmJsonApiModule { static getUtilProviders(entity: ObjectLiteral): NestProvider { return [ CurrentEntityRepository(entity), - TransformDataService, TypeormUtilsService, - ZodParamsFactory(), OrmServiceFactory(), FindOneRowEntityFactory(), CheckRelationNameFactory(), diff --git a/libs/microorm-database/src/lib/entities/comments.ts b/libs/microorm-database/src/lib/entities/comments.ts index 2b354d6d..3f4be0e3 100644 --- a/libs/microorm-database/src/lib/entities/comments.ts +++ b/libs/microorm-database/src/lib/entities/comments.ts @@ -48,6 +48,7 @@ export class Comments { @ManyToOne(() => Users, { fieldName: 'created_by', + nullable: true, }) createdBy!: IUsers; } diff --git a/libs/microorm-database/src/lib/entities/users.ts b/libs/microorm-database/src/lib/entities/users.ts index 703ac292..df71bb83 100644 --- a/libs/microorm-database/src/lib/entities/users.ts +++ b/libs/microorm-database/src/lib/entities/users.ts @@ -23,11 +23,10 @@ export class Users { public id!: number; @Property({ - // type: 'varchar', + type: 'varchar', length: 100, nullable: false, unique: true, - type: new ArrayType((i) => parseFloat(i)), }) public login!: string; @@ -97,7 +96,7 @@ export class Users { public manager!: IUsers; @OneToMany(() => Comments, (comment) => comment.createdBy) - comments = new Collection(this); + comments = new Collection(this); @ManyToMany(() => BookList, (item) => item.users, { owner: true, diff --git a/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts b/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts index cf00a86f..f7e2577d 100644 --- a/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts +++ b/libs/microorm-database/src/lib/migrations/Migration20250123123708_CreateUsersRolesRelations.ts @@ -1,16 +1,20 @@ import { Migration } from '@mikro-orm/migrations'; export class Migration20250123123708_CreateUsersRolesRelations extends Migration { - override async up(): Promise { - this.addSql(`create table "users_have_roles" ("users_id" int not null, "roles_id" int not null, constraint "users_have_roles_pkey" primary key ("users_id", "roles_id"));`); + this.addSql( + `create table "users_have_roles" ("users_id" int not null, "roles_id" int not null, constraint "users_have_roles_pkey" primary key ("users_id", "roles_id"));` + ); - this.addSql(`alter table "users_have_roles" add constraint "users_have_roles_users_id_foreign" foreign key ("users_id") references "users" ("id") on update cascade on delete cascade;`); - this.addSql(`alter table "users_have_roles" add constraint "users_have_roles_roles_id_foreign" foreign key ("roles_id") references "roles" ("id") on update cascade on delete cascade;`); + this.addSql( + `alter table "users_have_roles" add constraint "users_have_roles_users_id_foreign" foreign key ("users_id") references "users" ("id") on update cascade on delete cascade;` + ); + this.addSql( + `alter table "users_have_roles" add constraint "users_have_roles_roles_id_foreign" foreign key ("roles_id") references "roles" ("id") on update cascade on delete cascade;` + ); } override async down(): Promise { this.addSql(`drop table if exists "users_have_roles" cascade;`); } - } diff --git a/libs/typeorm-database/src/lib/entities/users-have-roles.ts b/libs/typeorm-database/src/lib/entities/users-have-roles.ts index 20d5d53c..38d400d2 100644 --- a/libs/typeorm-database/src/lib/entities/users-have-roles.ts +++ b/libs/typeorm-database/src/lib/entities/users-have-roles.ts @@ -11,7 +11,7 @@ export class UsersHaveRoles { public id!: number; @Column({ - name: 'user_id', + name: 'users_id', type: 'int', nullable: false, unique: false, @@ -19,7 +19,7 @@ export class UsersHaveRoles { public userId!: number; @Column({ - name: 'role_id', + name: 'roles_id', type: 'int', nullable: false, unique: false, diff --git a/libs/typeorm-database/src/lib/entities/users.ts b/libs/typeorm-database/src/lib/entities/users.ts index f0048c58..a709101e 100644 --- a/libs/typeorm-database/src/lib/entities/users.ts +++ b/libs/typeorm-database/src/lib/entities/users.ts @@ -96,11 +96,11 @@ export class Users { name: 'users_have_roles', inverseJoinColumn: { referencedColumnName: 'id', - name: 'role_id', + name: 'roles_id', }, joinColumn: { referencedColumnName: 'id', - name: 'user_id', + name: 'users_id', }, }) public roles!: IRoles[]; @@ -113,11 +113,11 @@ export class Users { name: 'users_have_book', inverseJoinColumn: { referencedColumnName: 'id', - name: 'book_id', + name: 'book_list_id', }, joinColumn: { referencedColumnName: 'id', - name: 'user_id', + name: 'users_id', }, }) public books!: IBookList[]; diff --git a/libs/typeorm-database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts b/libs/typeorm-database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts index fcd5ff95..6c0f068e 100644 --- a/libs/typeorm-database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts +++ b/libs/typeorm-database/src/lib/migrations/1607701632300-CreateUsersHaveRolesTable.ts @@ -26,13 +26,13 @@ export class CreateUsersHaveRolesTable1607701632300 generationStrategy: 'increment', }), new TableColumn({ - name: 'user_id', + name: 'users_id', type: 'int', isNullable: false, unsigned: true, }), new TableColumn({ - name: 'role_id', + name: 'roles_id', type: 'int', isNullable: false, unsigned: true, @@ -54,17 +54,17 @@ export class CreateUsersHaveRolesTable1607701632300 new TableForeignKey({ referencedTableName: 'users', referencedColumnNames: ['id'], - columnNames: ['user_id'], + columnNames: ['users_id'], }), new TableForeignKey({ referencedTableName: 'roles', referencedColumnNames: ['id'], - columnNames: ['role_id'], + columnNames: ['roles_id'], }), ], indices: [ new TableIndex({ - columnNames: ['user_id', 'role_id'], + columnNames: ['users_id', 'roles_id'], isUnique: true, }), ], diff --git a/libs/typeorm-database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts b/libs/typeorm-database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts index e713b4bf..d91ac132 100644 --- a/libs/typeorm-database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts +++ b/libs/typeorm-database/src/lib/migrations/1665719467563-CreateUsersHasBookTable.ts @@ -26,13 +26,13 @@ export class CreateUsersHaveBookTable1665719467563 generationStrategy: 'increment', }), new TableColumn({ - name: 'user_id', + name: 'users_id', type: 'int', isNullable: false, unsigned: true, }), new TableColumn({ - name: 'book_id', + name: 'book_list_id', type: 'uuid', isNullable: false, unsigned: true, @@ -54,17 +54,17 @@ export class CreateUsersHaveBookTable1665719467563 new TableForeignKey({ referencedTableName: 'users', referencedColumnNames: ['id'], - columnNames: ['user_id'], + columnNames: ['users_id'], }), new TableForeignKey({ referencedTableName: 'book_list', referencedColumnNames: ['id'], - columnNames: ['book_id'], + columnNames: ['book_list_id'], }), ], indices: [ new TableIndex({ - columnNames: ['user_id', 'book_id'], + columnNames: ['users_id', 'book_list_id'], isUnique: true, }), ], diff --git a/package-lock.json b/package-lock.json index 48805ff6..2a46cce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,6 +102,7 @@ "prettier": "^2.6.2", "ts-jest": "^29.1.0", "ts-node": "10.9.1", + "typeorm-pglite": "^0.3.2", "typescript": "5.6.3", "verdaccio": "^5.0.4", "webpack-cli": "^5.1.4" @@ -25335,6 +25336,15 @@ } } }, + "node_modules/typeorm-pglite": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/typeorm-pglite/-/typeorm-pglite-0.3.2.tgz", + "integrity": "sha512-/URkIx3MXaRqLKXjGfFY1uq8Fp6qSYTP09NjK6MAbsZhNwe7qAqnaWdTGtJ4aMr2e2TDPXT1rCt+w7xszMUzOg==", + "dev": true, + "peerDependencies": { + "@electric-sql/pglite": ">= 0.2.12" + } + }, "node_modules/typeorm/node_modules/buffer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", diff --git a/package.json b/package.json index f2157a87..4d3bccfd 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "prettier": "^2.6.2", "ts-jest": "^29.1.0", "ts-node": "10.9.1", + "typeorm-pglite": "^0.3.2", "typescript": "5.6.3", "verdaccio": "^5.0.4", "webpack-cli": "^5.1.4" From 19df57047b8857ec51bf714cd2f66b64d956c1ed Mon Sep 17 00:00:00 2001 From: Alex H Date: Tue, 11 Feb 2025 06:28:01 +0100 Subject: [PATCH 18/26] ci: exlude database from ci --- nx.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nx.json b/nx.json index 9a865fa3..01229c6f 100644 --- a/nx.json +++ b/nx.json @@ -85,7 +85,8 @@ "!json-api-server", "!json-api-front", "!shared-utils", - "!database", + "!typeorm-database", + "!microorm-database", "!type-for-rpc" ], "version": { From 5d59365613867b14ce2e015ee5665216bc67849d Mon Sep 17 00:00:00 2001 From: Alex H Date: Tue, 11 Feb 2025 06:39:57 +0100 Subject: [PATCH 19/26] test: debug test --- .github/workflows/ci.yml | 4 +- .../post-relationship/post-relationship.ts | 62 +++++++++++-------- .../service/micro-orm-util.service.ts | 2 +- 3 files changed, 38 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63fda5e8..0820cc30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,11 +30,11 @@ jobs: - name: Test env: NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 - run: npx nx affected -t test --parallel=3 --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,database,@nestjs-json-api/source,type-for-rpc' + run: npx nx affected -t test --parallel=3 --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,microorm-database,typeorm-database,@nestjs-json-api/source,type-for-rpc' - name: Build env: NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 - run: npx nx affected -t build --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,database,@nestjs-json-api/source,type-for-rpc' + run: npx nx affected -t build --exclude='json-api-front,json-api-server,json-api-server-e2e,json-shared-type,microorm-database,typeorm-database,@nestjs-json-api/source,type-for-rpc' - name: Save cached .nx id: cache-dependencies-save uses: actions/cache/save@v4 diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts index 5462417c..019daed4 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts @@ -14,35 +14,43 @@ export async function postRelationship< rel: Rel, input: PostRelationshipData ): Promise { - const idsResult = await this.microOrmUtilService.validateRelationInputData( - rel, - input - ); - const currentEntityRef = this.microOrmUtilService.entityManager.getReference( - this.microOrmUtilService.entity, - id as any - ); + try { + const idsResult = await this.microOrmUtilService.validateRelationInputData( + rel, + input + ); - const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); + const currentEntityRef = + this.microOrmUtilService.entityManager.getReference( + this.microOrmUtilService.entity, + id as any + ); - if (Array.isArray(idsResult)) { - const relRef = idsResult.map((i) => - this.microOrmUtilService.entityManager.getReference(relEntity, i as any) - ); - currentEntityRef[rel].add(...relRef); - } else { - // @ts-ignore - currentEntityRef[rel] = this.microOrmUtilService.entityManager.getReference( - relEntity, - idsResult as any - ); - } + const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); + + if (Array.isArray(idsResult)) { + const relRef = idsResult.map((i) => + this.microOrmUtilService.entityManager.getReference(relEntity, i as any) + ); + currentEntityRef[rel].add(...relRef); + } else { + // @ts-ignore + currentEntityRef[rel] = + this.microOrmUtilService.entityManager.getReference( + relEntity, + idsResult as any + ); + } - await this.microOrmUtilService.entityManager.flush(); + await this.microOrmUtilService.entityManager.flush(); - return getRelationship.call< - MicroOrmService, - Parameters>, - ReturnType> - >(this, id, rel); + return getRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel); + } catch (e) { + console.log(e); + throw e; + } } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts index dc7ff359..9bcc4e27 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts @@ -720,7 +720,7 @@ export class MicroOrmUtilService { }, }) .getResult(); - + console.log(checkResult, prepareData, rel, inputData); if (checkResult.length === prepareData.length) { return ( isArray ? inputData.map((i) => i.id) : inputData.id From 57b64574b7dfa1ec73d3d79312ac18d7f7634b51 Mon Sep 17 00:00:00 2001 From: Alex H Date: Tue, 11 Feb 2025 06:48:34 +0100 Subject: [PATCH 20/26] test: debug test --- .../post-relationship/post-relationship.ts | 61 ++++++++----------- .../service/micro-orm-util.service.ts | 2 +- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts index 019daed4..9fe683ac 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts @@ -14,43 +14,36 @@ export async function postRelationship< rel: Rel, input: PostRelationshipData ): Promise { - try { - const idsResult = await this.microOrmUtilService.validateRelationInputData( - rel, - input - ); + const idsResult = await this.microOrmUtilService.validateRelationInputData( + rel, + input + ); - const currentEntityRef = - this.microOrmUtilService.entityManager.getReference( - this.microOrmUtilService.entity, - id as any - ); + const currentEntityRef = this.microOrmUtilService.entityManager.getReference( + this.microOrmUtilService.entity, + id as any + ); - const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); + const relEntity = this.microOrmUtilService.getRelation(rel as any).entity(); - if (Array.isArray(idsResult)) { - const relRef = idsResult.map((i) => - this.microOrmUtilService.entityManager.getReference(relEntity, i as any) - ); - currentEntityRef[rel].add(...relRef); - } else { - // @ts-ignore - currentEntityRef[rel] = - this.microOrmUtilService.entityManager.getReference( - relEntity, - idsResult as any - ); - } + if (Array.isArray(idsResult)) { + const relRef = idsResult.map((i) => + this.microOrmUtilService.entityManager.getReference(relEntity, i as any) + ); + currentEntityRef[rel].add(...relRef); + } else { + // @ts-ignore + currentEntityRef[rel] = this.microOrmUtilService.entityManager.getReference( + relEntity, + idsResult as any + ); + } - await this.microOrmUtilService.entityManager.flush(); + await this.microOrmUtilService.entityManager.flush(); - return getRelationship.call< - MicroOrmService, - Parameters>, - ReturnType> - >(this, id, rel); - } catch (e) { - console.log(e); - throw e; - } + return getRelationship.call< + MicroOrmService, + Parameters>, + ReturnType> + >(this, id, rel); } diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts index 9bcc4e27..dc7ff359 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts @@ -720,7 +720,7 @@ export class MicroOrmUtilService { }, }) .getResult(); - console.log(checkResult, prepareData, rel, inputData); + if (checkResult.length === prepareData.length) { return ( isArray ? inputData.map((i) => i.id) : inputData.id From 69acaa8be9b6b8058335fc1dedcd9f218e52e039 Mon Sep 17 00:00:00 2001 From: Alex H Date: Tue, 11 Feb 2025 07:38:38 +0100 Subject: [PATCH 21/26] refactor(json-api-nestjs): move to project use as separete package in next version --- .../json-api-nestjs-sdk/src/shared/index.ts | 2 + .../src/shared/lib/types/entity-type.ts | 17 +++++ .../src/shared/lib/types/index.ts | 4 + .../src/shared/lib/types/query-type.ts | 36 +++++++++ .../src/shared/lib/types/response-body.ts | 76 +++++++++++++++++++ .../src/shared/lib/types/utils-string.type.ts | 26 +++++++ .../src/shared/lib/utils/index.ts | 2 + .../src/shared/lib/utils/object-utils.ts | 21 +++++ .../src/shared/lib/utils/string-utils.spec.ts | 42 ++++++++++ .../src/shared/lib/utils/string-utils.ts | 38 ++++++++++ libs/json-api/json-api-nestjs/src/index.ts | 2 +- .../src/lib/mock-utils/microrom/index.ts | 6 +- .../factory/map-entity-name-to-entity.ts | 2 +- .../service/execute.service.ts | 2 +- .../service/explorer.service.ts | 2 +- .../atomic-operation/utils/zod/zod-helper.ts | 2 +- .../lib/modules/micro-orm/factory/index.ts | 2 +- .../delete-relationship.spec.ts | 3 +- .../delete-relationship.ts | 2 +- .../get-all/get-query-for-count.spec.ts | 2 +- .../get-all/get-query-for-count.ts | 2 +- .../get-relationship/get-relationship.spec.ts | 4 +- .../get-relationship/get-relationship.ts | 2 +- .../orm-methods/patch-one/patch-one.ts | 3 +- .../patch-relationship.spec.ts | 2 +- .../patch-relationship/patch-relationship.ts | 2 +- .../post-relationship.spec.ts | 2 +- .../post-relationship/post-relationship.ts | 2 +- .../service/micro-orm-util.service.spec.ts | 2 +- .../service/micro-orm-util.service.ts | 2 +- .../micro-orm/service/microorm-service.ts | 2 +- .../src/lib/modules/mixin/config/bindings.ts | 2 +- .../mixin/controller/json-base.controller.ts | 2 +- .../mixin/factory/zod-validate.factory.ts | 2 +- .../mixin/helper/bind-controller.spec.ts | 2 +- .../modules/mixin/helper/create-controller.ts | 2 +- .../src/lib/modules/mixin/helper/utils.ts | 2 +- .../src/lib/modules/mixin/pipe/index.ts | 2 +- .../parse-relationship-name.pipe.ts | 2 +- .../query-check-select-field.spec.ts | 2 +- .../query-filed-in-include.pipe.spec.ts | 2 +- .../query-filed-in-include.pipe.ts | 2 +- .../mixin/pipe/query/query.pipe.spec.ts | 2 +- .../service/json-api-transformer.service.ts | 2 +- .../mixin/swagger/filter-operand-model.ts | 2 +- .../modules/mixin/swagger/method/get-all.ts | 2 +- .../modules/mixin/swagger/method/get-one.ts | 2 +- .../mixin/swagger/swagger-bind.service.ts | 2 +- .../src/lib/modules/mixin/swagger/utils.ts | 2 +- .../modules/mixin/types/orm-service.type.ts | 2 +- .../src/lib/modules/mixin/types/utils.ts | 2 +- .../zod/zod-input-query-schema/fields.ts | 2 +- .../zod/zod-input-query-schema/filter.ts | 2 +- .../zod/zod-input-query-schema/include.ts | 2 +- .../zod/zod-input-query-schema/index.spec.ts | 2 +- .../mixin/zod/zod-input-query-schema/index.ts | 2 +- .../mixin/zod/zod-query-schema/fields.ts | 2 +- .../mixin/zod/zod-query-schema/filter.ts | 2 +- .../mixin/zod/zod-query-schema/index.spec.ts | 2 +- .../mixin/zod/zod-query-schema/index.ts | 2 +- .../mixin/zod/zod-query-schema/sort.ts | 2 +- .../modules/mixin/zod/zod-share/attributes.ts | 2 +- .../mixin/zod/zod-share/relationships.ts | 2 +- .../src/lib/modules/type-orm/factory/index.ts | 2 +- .../modules/type-orm/orm-helper/index.spec.ts | 2 +- .../lib/modules/type-orm/orm-helper/index.ts | 2 +- .../delete-relationship.ts | 2 +- .../orm-methods/get-all/get-all.spec.ts | 2 +- .../type-orm/orm-methods/get-all/get-all.ts | 2 +- .../orm-methods/get-one/get-one.spec.ts | 2 +- .../type-orm/orm-methods/get-one/get-one.ts | 2 +- .../get-relationship/get-relationship.ts | 2 +- .../orm-methods/patch-one/patch-one.ts | 2 +- .../patch-relationship/patch-relationship.ts | 2 +- .../type-orm/orm-methods/post-one/post-one.ts | 2 +- .../post-relationship/post-relationship.ts | 2 +- .../type-orm/service/type-orm.service.ts | 2 +- .../service/typeorm-utils.service.spec.ts | 2 +- .../type-orm/service/typeorm-utils.service.ts | 2 +- .../src/lib/modules/type-orm/type.ts | 2 +- .../src/lib/utils/nestjs-shared/index.ts | 2 + .../nestjs-shared/lib/types/entity-type.ts | 17 +++++ .../utils/nestjs-shared/lib/types/index.ts | 4 + .../nestjs-shared/lib/types/query-type.ts | 36 +++++++++ .../nestjs-shared/lib/types/response-body.ts | 76 +++++++++++++++++++ .../lib/types/utils-string.type.ts | 26 +++++++ .../utils/nestjs-shared/lib/utils/index.ts | 2 + .../nestjs-shared/lib/utils/object-utils.ts | 21 +++++ .../lib/utils/string-utils.spec.ts | 42 ++++++++++ .../nestjs-shared/lib/utils/string-utils.ts | 38 ++++++++++ 90 files changed, 599 insertions(+), 77 deletions(-) create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/index.ts create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/entity-type.ts create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/index.ts create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/query-type.ts create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/response-body.ts create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/utils-string.type.ts create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/index.ts create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/object-utils.ts create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.spec.ts create mode 100644 libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/entity-type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/query-type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/response-body.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/utils-string.type.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/index.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/object-utils.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.spec.ts create mode 100644 libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.ts diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/index.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/index.ts new file mode 100644 index 00000000..a0fe9b9f --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/index.ts @@ -0,0 +1,2 @@ +export * from './lib/utils'; +export * from './lib/types'; diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/entity-type.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/entity-type.ts new file mode 100644 index 00000000..5fbdd04f --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/entity-type.ts @@ -0,0 +1,17 @@ +export type EntityField = + | string + | number + | bigint + | boolean + | string[] + | number[] + | null + | Date; + +export type EntityProps = { + [P in keyof T]: T[P] extends EntityField ? P : never; +}[keyof T]; + +export type EntityRelation = { + [P in keyof T]: T[P] extends EntityField ? never : P; +}[keyof T]; diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/index.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/index.ts new file mode 100644 index 00000000..33b70ba6 --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/index.ts @@ -0,0 +1,4 @@ +export * from './utils-string.type'; +export * from './query-type'; +export * from './entity-type'; +export * from './response-body'; diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/query-type.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/query-type.ts new file mode 100644 index 00000000..ddeead7a --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/query-type.ts @@ -0,0 +1,36 @@ +export enum QueryField { + filter = 'filter', + sort = 'sort', + include = 'include', + page = 'page', + fields = 'fields', +} + +export enum FilterOperand { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', + in = 'in', + nin = 'nin', + some = 'some', +} + +export enum FilterOperandOnlyInNin { + in = 'in', + nin = 'nin', +} +export enum FilterOperandOnlySimple { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', +} diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/response-body.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/response-body.ts new file mode 100644 index 00000000..85dbdb49 --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/response-body.ts @@ -0,0 +1,76 @@ +import { + EntityField, + EntityProps, + EntityRelation, + TypeOfArray, + ValueOf, +} from '.'; +import { Collection } from '@mikro-orm/core'; + +export type PageProps = { + totalItems: number; + pageNumber: number; + pageSize: number; +}; + +export type DebugMetaProps = Partial<{ + time: number; +}>; + +export type MainData = { + type: T; + id: string; +}; + +export type Links = { + self: string; + related?: string; +}; + +export type Attributes = { + [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; +}; + +export type DataResult = E extends unknown[] + ? MainData[] + : E extends Collection + ? MainData[] + : MainData | null; + +export type Data = { + data?: DataResult; +}; + +export type Relationships = { + [P in EntityRelation]?: { + links: Links; + } & Data; +}; + +export type Include = ValueOf<{ + [P in EntityRelation]: ResourceData>; +}>; + +export type ResourceData = MainData & { + attributes?: Attributes; + relationships?: Relationships; + links: Omit; +}; + +export type MetaProps = R extends null ? T : T & R; + +export type ResourceObject< + T, + R extends 'object' | 'array' = 'object', + M = null +> = { + meta: R extends 'array' + ? MetaProps + : MetaProps; + data: R extends 'array' ? ResourceData[] : ResourceData; + included?: Include[]; +}; + +export type ResourceObjectRelationships> = { + meta: DebugMetaProps; +} & Required>; diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/utils-string.type.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/utils-string.type.ts new file mode 100644 index 00000000..b861d238 --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/types/utils-string.type.ts @@ -0,0 +1,26 @@ +import type { Collection } from '@mikro-orm/core'; + +export type KebabCase = S extends `${infer C}${infer T}` + ? KebabCase extends infer U + ? U extends string + ? T extends Uncapitalize + ? `${Uncapitalize}${U}` + : `${Uncapitalize}-${U}` + : never + : never + : S; + +export type KebabToCamelCase = + S extends `${infer T}-${infer U}-${infer V}` + ? `${T}${Capitalize}${Capitalize>}` + : S extends `${infer T}-${infer U}` + ? `${Capitalize}${Capitalize>}` + : S; + +export type TypeOfArray = T extends (infer U)[] + ? U + : T extends Collection + ? U + : T; + +export type ValueOf = T[keyof T]; diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/index.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/index.ts new file mode 100644 index 00000000..a7799257 --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/index.ts @@ -0,0 +1,2 @@ +export * from './string-utils'; +export * from './object-utils'; diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/object-utils.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/object-utils.ts new file mode 100644 index 00000000..5d4ad26d --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/object-utils.ts @@ -0,0 +1,21 @@ +import { kebabToCamel } from './string-utils'; + +export const ObjectTyped = { + keys: Object.keys as (yourObject: T) => Array, + values: Object.values as (yourObject: U) => Array, + entries: Object.entries as ( + yourObject: O + ) => Array<[keyof O, O[keyof O]]>, + fromEntries: Object.fromEntries as ( + yourObjectEntries: [K, V][] + ) => Record, +}; + +export function isObject(item: unknown): item is object { + return typeof item === 'object' && !Array.isArray(item) && item !== null; +} + +export function createEntityInstance(name: string): E { + const entityName = kebabToCamel(name); + return Function('return new class ' + entityName + '{}')(); +} diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.spec.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.spec.ts new file mode 100644 index 00000000..4596cd21 --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.spec.ts @@ -0,0 +1,42 @@ +import { + camelToKebab, + snakeToCamel, + isString, + kebabToCamel, +} from './string-utils'; + +describe('Test utils', () => { + it('camelToKebab', () => { + const result = camelToKebab('ApproverGroups'); + const result1 = camelToKebab('Users'); + + expect(result).toBe('approver-groups'); + expect(result1).toBe('users'); + }); + + it('snakeToCamel', () => { + const result = snakeToCamel('test_test'); + const result1 = snakeToCamel('test-test'); + const result2 = snakeToCamel('testTest'); + const result3 = snakeToCamel('event_incident_typeFK'); + expect(result).toBe('testTest'); + expect(result1).toBe('testTest'); + expect(result2).toBe('testTest'); + expect(result3).toBe('eventIncidentTypeFK'); + }); + + it('isString', () => { + expect(isString('string')).toBe(true); + expect(isString(String('string'))).toBe(true); + expect(isString(new Date())).toBe(false); + expect(isString(class {})).toBe(false); + }); + + it('kebabToCamel', () => { + const type = 'users-group'; + const type1 = 'users'; + + expect(kebabToCamel(type)).toBe('UsersGroup'); + expect(kebabToCamel(type1)).toBe('Users'); + }); +}); diff --git a/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.ts b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.ts new file mode 100644 index 00000000..3b678710 --- /dev/null +++ b/libs/json-api/json-api-nestjs-sdk/src/shared/lib/utils/string-utils.ts @@ -0,0 +1,38 @@ +import { KebabToCamelCase, KebabCase } from '../types'; + +export function isString(value: T): value is P { + return typeof value === 'string' || value instanceof String; +} + +export function snakeToCamel(str: string): string { + if (!str.match(/[\s_-]/g)) { + return str; + } + return str.replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace('-', '').replace('_', '') + ); +} + +export function camelToKebab(string: S): KebabCase { + return string + .replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1') + .toLowerCase() as KebabCase; +} + +export function upperFirstLetter(string: S): Capitalize { + return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize; +} + +export function kebabToCamel(str: S): KebabToCamelCase { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join('') as KebabToCamelCase; +} + +export function capitalizeFirstChar(str: string) { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join(''); +} diff --git a/libs/json-api/json-api-nestjs/src/index.ts b/libs/json-api/json-api-nestjs/src/index.ts index c9d708b3..50c6ef0e 100644 --- a/libs/json-api/json-api-nestjs/src/index.ts +++ b/libs/json-api/json-api-nestjs/src/index.ts @@ -17,7 +17,7 @@ export { ResourceObject, ResourceObjectRelationships, QueryField, -} from '@klerick/json-api-nestjs-shared'; +} from './lib/utils/nestjs-shared'; export { excludeMethod } from './lib/modules/mixin/config/bindings'; export { entityForClass } from './lib/utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts index 03279fd7..9c542b6d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts @@ -2,11 +2,7 @@ import { DynamicModule } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { EntityManager, MikroORM } from '@mikro-orm/core'; -import { PostgreSqlDriver } from '@mikro-orm/postgresql'; -import { SqlHighlighter } from '@mikro-orm/sql-highlighter'; -import { QueryField } from '@klerick/json-api-nestjs-shared'; - -import { IMemoryDb } from 'pg-mem'; +import { QueryField } from '../../utils/nestjs-shared'; import { Addresses, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts index 50a1207a..848eea94 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/factory/map-entity-name-to-entity.ts @@ -1,6 +1,6 @@ import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type'; import { ValueProvider } from '@nestjs/common'; -import { camelToKebab } from '@klerick/json-api-nestjs-shared'; +import { camelToKebab } from '../../../utils/nestjs-shared'; import { MapEntity } from '../types'; import { MAP_ENTITY } from '../constants'; import { getEntityName } from '../../mixin/helper'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts index 0afc0f95..a59606ed 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/execute.service.ts @@ -17,7 +17,7 @@ import { ObjectTyped, ResourceObject, ResourceObjectRelationships, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { InterceptorsConsumer, InterceptorsContextCreator, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts index 1ad13223..57bc72b4 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/service/explorer.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, Type } from '@nestjs/common'; import { Module } from '@nestjs/core/injector/module'; import { ModulesContainer } from '@nestjs/core'; -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../utils/nestjs-shared'; import { MAP_CONTROLLER_ENTITY, MAP_ENTITY } from '../constants'; import { MapController, MapEntity, OperationMethode } from '../types'; import { ObjectLiteral as Entity } from '../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts index fb73822a..a1b83422 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/atomic-operation/utils/zod/zod-helper.ts @@ -9,7 +9,7 @@ import { ZodType, ZodUnion, } from 'zod'; -import { camelToKebab } from '@klerick/json-api-nestjs-shared'; +import { camelToKebab } from '../../../../utils/nestjs-shared'; import { KEY_MAIN_INPUT_SCHEMA } from '../../constants'; import { MapController } from '../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts index ad5a42d4..b8a13acc 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/factory/index.ts @@ -5,7 +5,7 @@ import { EntityRepository, MetadataStorage, } from '@mikro-orm/core'; -import { camelToKebab } from '@klerick/json-api-nestjs-shared'; +import { camelToKebab } from '../../../utils/nestjs-shared'; import { getMikroORMToken } from '@mikro-orm/nestjs'; import { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts index ceebf884..16004280 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.spec.ts @@ -21,8 +21,7 @@ import { } from '../../../../constants'; import { deleteRelationship } from './delete-relationship'; -import { BadRequestException } from '@nestjs/common'; -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; describe('delete-relationship', () => { let mikroORMUsers: MikroORM; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts index 881b1d27..1607e545 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/delete-relationship/delete-relationship.ts @@ -1,4 +1,4 @@ -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; import { ObjectLiteral, ValidateQueryError } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts index 8192be1d..3364e14e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.spec.ts @@ -1,5 +1,5 @@ import { EntityManager, MikroORM } from '@mikro-orm/core'; -import { FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { FilterOperand } from '../../../../utils/nestjs-shared'; import { UserGroups, Users, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts index 8dbde638..ccc2dda6 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-all/get-query-for-count.ts @@ -1,4 +1,4 @@ -import { ObjectTyped, ResourceObject } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; import { MicroOrmService } from '../../service'; import { Query } from '../../../mixin/zod'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts index 43958666..4454590c 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.spec.ts @@ -26,8 +26,8 @@ import { } from '../../../../constants'; import { getRelationship } from './get-relationship'; -import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { NotFoundException } from '@nestjs/common'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; describe('get-relationship', () => { let mikroORMUsers: MikroORM; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts index dd752173..09dbd99e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/get-relationship/get-relationship.ts @@ -1,4 +1,4 @@ -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; import { NotFoundException } from '@nestjs/common'; import { MicroOrmService } from '../../service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts index dc7590cd..3897f3c9 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-one/patch-one.ts @@ -2,8 +2,7 @@ import { NotFoundException, UnprocessableEntityException, } from '@nestjs/common'; -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; - +import { ObjectTyped } from '../../../../utils/nestjs-shared'; import { ObjectLiteral, ValidateQueryError } from '../../../../types'; import { MicroOrmService } from '../../service'; import { PatchData } from '../../../mixin/zod'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts index 643dca0d..5018a6b2 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.spec.ts @@ -21,7 +21,7 @@ import { } from '../../../../constants'; import { patchRelationship } from './patch-relationship'; -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; describe('patch-relationship', () => { let mikroORMUsers: MikroORM; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts index 2a6d4b71..38074d38 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/patch-relationship/patch-relationship.ts @@ -1,4 +1,4 @@ -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; import { ObjectLiteral } from '../../../../types'; import { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts index bbbeb4c2..2444afa3 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.spec.ts @@ -22,7 +22,7 @@ import { import { postRelationship } from './post-relationship'; import { BadRequestException } from '@nestjs/common'; -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; describe('post-relationshipa', () => { let mikroORMUsers: MikroORM; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts index 9fe683ac..77e8edcb 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/orm-methods/post-relationship/post-relationship.ts @@ -1,4 +1,4 @@ -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; import { ObjectLiteral } from '../../../../types'; import { PostRelationshipData } from '../../../mixin/zod'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts index 679c16d9..49af1af9 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.spec.ts @@ -1,4 +1,4 @@ -import { FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { FilterOperand } from '../../../utils/nestjs-shared'; import { MikroORM, RawQueryFragment } from '@mikro-orm/core'; import { TestingModule } from '@nestjs/testing/testing-module'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts index dc7ff359..3401d88d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/micro-orm-util.service.ts @@ -28,7 +28,7 @@ import { EntityRelation, FilterOperand, ObjectTyped, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { ASC, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts index 8c2d28a7..2cf332fe 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/micro-orm/service/microorm-service.ts @@ -3,7 +3,7 @@ import { QueryField, ResourceObject, ResourceObjectRelationships, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { Inject } from '@nestjs/common'; import { ObjectLiteral } from '../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts index 45eb5708..346a17df 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/config/bindings.ts @@ -1,5 +1,5 @@ import { Body, Param, Query, RequestMethod } from '@nestjs/common'; -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../utils/nestjs-shared'; import { BindingsConfig, MethodName } from '../types'; import { JsonBaseController } from '../controller/json-base.controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts index 28aa9ce7..146d54eb 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/controller/json-base.controller.ts @@ -2,7 +2,7 @@ import { EntityRelation, ResourceObject, ResourceObjectRelationships, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { ORM_SERVICE_PROPS } from '../../../constants'; import { MethodName } from '../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts index 5e29384f..942c24c6 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/factory/zod-validate.factory.ts @@ -38,7 +38,7 @@ import { ResultGetField, ZodEntityProps, } from '../types'; -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../utils/nestjs-shared'; function getEntityMap( entityMapProps: Map, ZodEntityProps>, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts index 2d0603f7..bf60890e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/bind-controller.spec.ts @@ -3,7 +3,7 @@ import { PATH_METADATA, ROUTE_ARGS_METADATA, } from '@nestjs/common/constants'; -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../utils/nestjs-shared'; import { RouteParamtypes } from '@nestjs/common/enums/route-paramtypes.enum'; import { bindController } from './bind-controller'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts index bab373a3..7a10058a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/create-controller.ts @@ -1,6 +1,6 @@ import { Controller, Inject, Type, UseInterceptors } from '@nestjs/common'; -import { camelToKebab } from '@klerick/json-api-nestjs-shared'; +import { camelToKebab } from '../../../utils/nestjs-shared'; import { getProviderName, nameIt } from './utils'; import { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts index 8aa2b798..88ebd7cc 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/helper/utils.ts @@ -1,6 +1,6 @@ import { EntityTarget, ObjectLiteral } from '../../../types'; -import { upperFirstLetter } from '@klerick/json-api-nestjs-shared'; +import { upperFirstLetter } from '../../../utils/nestjs-shared'; export const nameIt = ( name: string, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts index ee2bf865..b7c0cc13 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/index.ts @@ -1,5 +1,5 @@ import { Injectable, ParseIntPipe } from '@nestjs/common'; -import { upperFirstLetter } from '@klerick/json-api-nestjs-shared'; +import { upperFirstLetter } from '../../../utils/nestjs-shared'; import { PipeMixin } from '../../../types'; import { MixinOptions } from '../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts index 2224ec0b..de555df3 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/parse-relationship-name/parse-relationship-name.pipe.ts @@ -3,7 +3,7 @@ import { UnprocessableEntityException, Inject, } from '@nestjs/common'; -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; import { ValidateQueryError } from '../../../../types'; import { CHECK_RELATION_NAME, CURRENT_ENTITY } from '../../../../constants'; import { EntityTarget, ObjectLiteral } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts index a0f88160..161204f6 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-check-select-field/query-check-select-field.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BadRequestException } from '@nestjs/common'; -import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { QueryField } from '../../../../utils/nestjs-shared'; import { QueryCheckSelectField } from './query-check-select-field'; import { Users } from '../../../../mock-utils/typeorm'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts index fbfa1ce0..71dc33e4 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.spec.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { QueryField } from '../../../../utils/nestjs-shared'; import { QueryFiledInIncludePipe } from './query-filed-in-include.pipe'; import { Users } from '../../../../mock-utils/typeorm'; import { Query } from '../../zod'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts index dc27f7f4..8ce987eb 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query-filed-on-include/query-filed-in-include.pipe.ts @@ -1,5 +1,5 @@ import { BadRequestException, PipeTransform } from '@nestjs/common'; -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; import { ObjectLiteral, ValidateQueryError } from '../../../../types'; import { Query } from '../../zod'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts index cfb58c24..6f62baa4 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/pipe/query/query.pipe.spec.ts @@ -8,7 +8,7 @@ import { ZodError } from 'zod'; import { QueryPipe } from './query.pipe'; import { ASC, ZOD_QUERY_SCHEMA } from '../../../../constants'; import { ZodQuery, InputQuery, Query } from '../../zod'; -import { FilterOperand, QueryField } from '@klerick/json-api-nestjs-shared'; +import { FilterOperand, QueryField } from '../../../../utils/nestjs-shared'; type MockEntity = { id: number; name: string }; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts index 974473b5..6cd7bc6b 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/service/json-api-transformer.service.ts @@ -12,7 +12,7 @@ import { Relationships, ResourceData, ResourceObject, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { EntityClass, ObjectLiteral } from '../../../types'; import { ENTITY_MAP_PROPS, CURRENT_ENTITY } from '../../../constants'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts index a71b36ee..c5ae1563 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/filter-operand-model.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { FilterOperand as FilterOperandType } from '@klerick/json-api-nestjs-shared'; +import { FilterOperand as FilterOperandType } from '../../../utils/nestjs-shared'; const title = 'is equal to the conditional of query'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts index cc60cd6e..772290dd 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-all.ts @@ -1,6 +1,6 @@ import { ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; import { Type } from '@nestjs/common'; -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; import { EntityClass, ObjectLiteral } from '../../../../types'; import { ZodEntityProps } from '../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts index 2b0957c1..05fdb3d7 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/method/get-one.ts @@ -1,5 +1,5 @@ import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger'; -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; import { Type } from '@nestjs/common'; import { TypeField, ZodEntityProps } from '../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts index d37778f7..383607b3 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/swagger-bind.service.ts @@ -2,7 +2,7 @@ import { Injectable, OnModuleInit, Inject } from '@nestjs/common'; import { DECORATORS } from '@nestjs/swagger/dist/constants'; import { ApiExtraModels, ApiTags } from '@nestjs/swagger'; import { DiscoveryService } from '@nestjs/core'; -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../utils/nestjs-shared'; import { PARAMTYPES_METADATA } from '@nestjs/common/constants'; import { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts index 63fa0408..85607ea1 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/swagger/utils.ts @@ -4,7 +4,7 @@ import { ObjectTyped, EntityRelation, camelToKebab, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { EntityProps, TypeField, ZodEntityProps, ZodParams } from '../types'; import { EntityClass, ObjectLiteral } from '../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts index b6a501ac..546a8303 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/orm-service.type.ts @@ -4,7 +4,7 @@ import { EntityRelation, ResourceObject, ResourceObjectRelationships, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { PatchData, PatchRelationshipData, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts index 821a02fb..c254ca95 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/types/utils.ts @@ -5,7 +5,7 @@ import { EntityRelation, TypeOfArray, EntityProps, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { ObjectLiteral as Entity } from '../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts index 9bd95c11..93d94c81 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/fields.ts @@ -1,4 +1,4 @@ -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; import { z } from 'zod'; import { ObjectLiteral } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts index 9586fcbb..53f69f68 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/filter.ts @@ -2,7 +2,7 @@ import { FilterOperand, ObjectTyped, isString, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../../utils/nestjs-shared'; import { z } from 'zod'; import { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts index 639d34c8..b5a72ad8 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/include.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { isString } from '@klerick/json-api-nestjs-shared'; +import { isString } from '../../../../utils/nestjs-shared'; import { ZodInfer } from '../../types'; export function zodIncludeInputQuery() { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts index 48a3f61d..dc0691c7 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.spec.ts @@ -1,4 +1,4 @@ -import { QueryField, ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { QueryField, ObjectTyped } from '../../../../utils/nestjs-shared'; import { zodInputQuery } from './index'; import { ResultGetField, TupleOfEntityRelation } from '../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts index 04e80722..ac58dd4d 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-input-query-schema/index.ts @@ -1,4 +1,4 @@ -import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { QueryField } from '../../../../utils/nestjs-shared'; import { z } from 'zod'; import { RelationTree, ResultGetField } from '../../types'; import { ObjectLiteral } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts index b301c636..3d647940 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts @@ -1,4 +1,4 @@ -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; import { z } from 'zod'; import { nonEmptyObject, uniqueArray } from '../zod-utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts index 9653d77d..5e92b607 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts @@ -4,7 +4,7 @@ import { ObjectTyped, FilterOperandOnlyInNin, FilterOperandOnlySimple, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../../utils/nestjs-shared'; import { AllFieldWithType, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts index e75e6e1e..93ca9fc3 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.spec.ts @@ -1,4 +1,4 @@ -import { FilterOperand, QueryField } from '@klerick/json-api-nestjs-shared'; +import { FilterOperand, QueryField } from '../../../../utils/nestjs-shared'; import { zodQuery } from './index'; import { ArrayPropsForEntity } from '../../types'; import { Users } from '../../../../mock-utils/typeorm'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts index 4025636f..d984dded 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts @@ -1,4 +1,4 @@ -import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { QueryField } from '../../../../utils/nestjs-shared'; import { z, ZodObject } from 'zod'; import { ObjectLiteral } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts index f129fdb0..7951749e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/sort.ts @@ -1,4 +1,4 @@ -import { ObjectTyped } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped } from '../../../../utils/nestjs-shared'; import { z } from 'zod'; import { RelationTree, ResultGetField } from '../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts index d00503f7..1f29bcdd 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/attributes.ts @@ -2,7 +2,7 @@ import { EntityProps, ObjectTyped, TypeOfArray, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../../utils/nestjs-shared'; import { z, ZodArray, ZodNullable } from 'zod'; import { ObjectLiteral } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts index 425253b8..3a6dd520 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-share/relationships.ts @@ -3,7 +3,7 @@ import { camelToKebab, KebabCase, ObjectTyped, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../../utils/nestjs-shared'; import { ObjectLiteral } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts index 0c365cb1..bfc13543 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/factory/index.ts @@ -1,6 +1,6 @@ import { FactoryProvider } from '@nestjs/common'; import { getDataSourceToken } from '@nestjs/typeorm'; -import { camelToKebab } from '@klerick/json-api-nestjs-shared'; +import { camelToKebab } from '../../../utils/nestjs-shared'; import { DataSource, EntityManager, Repository } from 'typeorm'; import { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts index 35cb3fae..8a06a3df 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.spec.ts @@ -5,7 +5,7 @@ import { EntityRelation, TypeOfArray, EntityProps, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { Repository } from 'typeorm'; import { IMemoryDb } from 'pg-mem'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts index a96321e9..4657b4be 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-helper/index.ts @@ -2,7 +2,7 @@ import { EntityProps, EntityRelation, ObjectTyped, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { Repository } from 'typeorm'; import { ObjectLiteral, ResultMicroOrmModuleOptions } from '../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts index 141b41a8..947553b5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/delete-relationship/delete-relationship.ts @@ -1,4 +1,4 @@ -import { EntityRelation } from '@klerick/json-api-nestjs-shared'; +import { EntityRelation } from '../../../../utils/nestjs-shared'; import { ObjectLiteral } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts index a55b95c2..3dd8ea02 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getDataSourceToken } from '@nestjs/typeorm'; -import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { QueryField } from '../../../../utils/nestjs-shared'; import { Equal, Repository } from 'typeorm'; import { IMemoryDb } from 'pg-mem'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts index 8e098e76..e6c9fa3e 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-all/get-all.ts @@ -1,4 +1,4 @@ -import { ObjectTyped, ResourceObject } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped, ResourceObject } from '../../../../utils/nestjs-shared'; import { ObjectLiteral } from '../../../../types'; import { Query } from '../../../mixin/zod'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts index 53c9d874..97fb1f30 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.spec.ts @@ -1,6 +1,6 @@ import { getDataSourceToken } from '@nestjs/typeorm'; import { Test, TestingModule } from '@nestjs/testing'; -import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { QueryField } from '../../../../utils/nestjs-shared'; import { IMemoryDb } from 'pg-mem'; import { Equal, Repository } from 'typeorm'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts index 42e58c61..b092393f 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-one/get-one.ts @@ -1,5 +1,5 @@ import { NotFoundException } from '@nestjs/common'; -import { ObjectTyped, ResourceObject } from '@klerick/json-api-nestjs-shared'; +import { ObjectTyped, ResourceObject } from '../../../../utils/nestjs-shared'; import { ObjectLiteral, ValidateQueryError } from '../../../../types'; import { QueryOne } from '../../../mixin/zod'; import { TypeOrmService } from '../../service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts index 5a76cc87..be447e90 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/get-relationship/get-relationship.ts @@ -1,7 +1,7 @@ import { EntityRelation, ResourceObjectRelationships, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../../utils/nestjs-shared'; import { InternalServerErrorException, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts index 70c613cf..5551a31b 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-one/patch-one.ts @@ -7,7 +7,7 @@ import { ResourceObject, ObjectTyped, QueryField, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../../utils/nestjs-shared'; import { ObjectLiteral, ValidateQueryError } from '../../../../types'; import { PatchData, Query } from '../../../mixin/zod'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts index c8ed4f81..2ccbb0dd 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/patch-relationship/patch-relationship.ts @@ -1,7 +1,7 @@ import { EntityRelation, ResourceObjectRelationships, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../../utils/nestjs-shared'; import { ObjectLiteral } from '../../../../types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts index 4e85e139..08ac0a1a 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-one/post-one.ts @@ -1,5 +1,5 @@ import { DeepPartial } from 'typeorm'; -import { QueryField, ResourceObject } from '@klerick/json-api-nestjs-shared'; +import { QueryField, ResourceObject } from '../../../../utils/nestjs-shared'; import { ObjectLiteral } from '../../../../types'; import { PostData, Query } from '../../../mixin/zod'; import { TypeOrmService } from '../../service'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts index 695c9ff1..7719cea0 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/orm-methods/post-relationship/post-relationship.ts @@ -1,7 +1,7 @@ import { EntityRelation, ResourceObjectRelationships, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../../utils/nestjs-shared'; import { ObjectLiteral } from '../../../../types'; import { PostRelationshipData } from '../../../mixin/zod'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts index b17d6c20..1e040512 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/type-orm.service.ts @@ -2,7 +2,7 @@ import { ResourceObject, EntityRelation, ResourceObjectRelationships, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { Inject } from '@nestjs/common'; import { Repository } from 'typeorm'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts index 817e04f1..c53036c5 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getDataSourceToken } from '@nestjs/typeorm'; -import { QueryField, FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { QueryField, FilterOperand } from '../../../utils/nestjs-shared'; import { BadRequestException, NotFoundException, diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts index 146b0687..ecdd35c9 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/service/typeorm-utils.service.ts @@ -13,7 +13,7 @@ import { ObjectTyped, snakeToCamel, FilterOperand, -} from '@klerick/json-api-nestjs-shared'; +} from '../../../utils/nestjs-shared'; import { ObjectLiteral, ValidateQueryError } from '../../../types'; import { diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts index edea06bc..9d365343 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/type-orm/type.ts @@ -1,5 +1,5 @@ import { IsolationLevel } from 'typeorm/driver/types/IsolationLevel'; -import { FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { FilterOperand } from '../../utils/nestjs-shared'; export type TypeOrmParam = { useSoftDelete?: boolean; runInTransaction?: any>( diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/index.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/index.ts new file mode 100644 index 00000000..a0fe9b9f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/index.ts @@ -0,0 +1,2 @@ +export * from './lib/utils'; +export * from './lib/types'; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/entity-type.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/entity-type.ts new file mode 100644 index 00000000..5fbdd04f --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/entity-type.ts @@ -0,0 +1,17 @@ +export type EntityField = + | string + | number + | bigint + | boolean + | string[] + | number[] + | null + | Date; + +export type EntityProps = { + [P in keyof T]: T[P] extends EntityField ? P : never; +}[keyof T]; + +export type EntityRelation = { + [P in keyof T]: T[P] extends EntityField ? never : P; +}[keyof T]; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/index.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/index.ts new file mode 100644 index 00000000..33b70ba6 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/index.ts @@ -0,0 +1,4 @@ +export * from './utils-string.type'; +export * from './query-type'; +export * from './entity-type'; +export * from './response-body'; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/query-type.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/query-type.ts new file mode 100644 index 00000000..ddeead7a --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/query-type.ts @@ -0,0 +1,36 @@ +export enum QueryField { + filter = 'filter', + sort = 'sort', + include = 'include', + page = 'page', + fields = 'fields', +} + +export enum FilterOperand { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', + in = 'in', + nin = 'nin', + some = 'some', +} + +export enum FilterOperandOnlyInNin { + in = 'in', + nin = 'nin', +} +export enum FilterOperandOnlySimple { + eq = 'eq', + gt = 'gt', + gte = 'gte', + like = 'like', + lt = 'lt', + lte = 'lte', + ne = 'ne', + regexp = 'regexp', +} diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/response-body.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/response-body.ts new file mode 100644 index 00000000..85dbdb49 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/response-body.ts @@ -0,0 +1,76 @@ +import { + EntityField, + EntityProps, + EntityRelation, + TypeOfArray, + ValueOf, +} from '.'; +import { Collection } from '@mikro-orm/core'; + +export type PageProps = { + totalItems: number; + pageNumber: number; + pageSize: number; +}; + +export type DebugMetaProps = Partial<{ + time: number; +}>; + +export type MainData = { + type: T; + id: string; +}; + +export type Links = { + self: string; + related?: string; +}; + +export type Attributes = { + [P in EntityProps]?: D[P] extends EntityField ? D[P] : TypeOfArray; +}; + +export type DataResult = E extends unknown[] + ? MainData[] + : E extends Collection + ? MainData[] + : MainData | null; + +export type Data = { + data?: DataResult; +}; + +export type Relationships = { + [P in EntityRelation]?: { + links: Links; + } & Data; +}; + +export type Include = ValueOf<{ + [P in EntityRelation]: ResourceData>; +}>; + +export type ResourceData = MainData & { + attributes?: Attributes; + relationships?: Relationships; + links: Omit; +}; + +export type MetaProps = R extends null ? T : T & R; + +export type ResourceObject< + T, + R extends 'object' | 'array' = 'object', + M = null +> = { + meta: R extends 'array' + ? MetaProps + : MetaProps; + data: R extends 'array' ? ResourceData[] : ResourceData; + included?: Include[]; +}; + +export type ResourceObjectRelationships> = { + meta: DebugMetaProps; +} & Required>; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/utils-string.type.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/utils-string.type.ts new file mode 100644 index 00000000..b861d238 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/types/utils-string.type.ts @@ -0,0 +1,26 @@ +import type { Collection } from '@mikro-orm/core'; + +export type KebabCase = S extends `${infer C}${infer T}` + ? KebabCase extends infer U + ? U extends string + ? T extends Uncapitalize + ? `${Uncapitalize}${U}` + : `${Uncapitalize}-${U}` + : never + : never + : S; + +export type KebabToCamelCase = + S extends `${infer T}-${infer U}-${infer V}` + ? `${T}${Capitalize}${Capitalize>}` + : S extends `${infer T}-${infer U}` + ? `${Capitalize}${Capitalize>}` + : S; + +export type TypeOfArray = T extends (infer U)[] + ? U + : T extends Collection + ? U + : T; + +export type ValueOf = T[keyof T]; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/index.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/index.ts new file mode 100644 index 00000000..a7799257 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/index.ts @@ -0,0 +1,2 @@ +export * from './string-utils'; +export * from './object-utils'; diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/object-utils.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/object-utils.ts new file mode 100644 index 00000000..5d4ad26d --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/object-utils.ts @@ -0,0 +1,21 @@ +import { kebabToCamel } from './string-utils'; + +export const ObjectTyped = { + keys: Object.keys as (yourObject: T) => Array, + values: Object.values as (yourObject: U) => Array, + entries: Object.entries as ( + yourObject: O + ) => Array<[keyof O, O[keyof O]]>, + fromEntries: Object.fromEntries as ( + yourObjectEntries: [K, V][] + ) => Record, +}; + +export function isObject(item: unknown): item is object { + return typeof item === 'object' && !Array.isArray(item) && item !== null; +} + +export function createEntityInstance(name: string): E { + const entityName = kebabToCamel(name); + return Function('return new class ' + entityName + '{}')(); +} diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.spec.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.spec.ts new file mode 100644 index 00000000..4596cd21 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.spec.ts @@ -0,0 +1,42 @@ +import { + camelToKebab, + snakeToCamel, + isString, + kebabToCamel, +} from './string-utils'; + +describe('Test utils', () => { + it('camelToKebab', () => { + const result = camelToKebab('ApproverGroups'); + const result1 = camelToKebab('Users'); + + expect(result).toBe('approver-groups'); + expect(result1).toBe('users'); + }); + + it('snakeToCamel', () => { + const result = snakeToCamel('test_test'); + const result1 = snakeToCamel('test-test'); + const result2 = snakeToCamel('testTest'); + const result3 = snakeToCamel('event_incident_typeFK'); + expect(result).toBe('testTest'); + expect(result1).toBe('testTest'); + expect(result2).toBe('testTest'); + expect(result3).toBe('eventIncidentTypeFK'); + }); + + it('isString', () => { + expect(isString('string')).toBe(true); + expect(isString(String('string'))).toBe(true); + expect(isString(new Date())).toBe(false); + expect(isString(class {})).toBe(false); + }); + + it('kebabToCamel', () => { + const type = 'users-group'; + const type1 = 'users'; + + expect(kebabToCamel(type)).toBe('UsersGroup'); + expect(kebabToCamel(type1)).toBe('Users'); + }); +}); diff --git a/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.ts b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.ts new file mode 100644 index 00000000..3b678710 --- /dev/null +++ b/libs/json-api/json-api-nestjs/src/lib/utils/nestjs-shared/lib/utils/string-utils.ts @@ -0,0 +1,38 @@ +import { KebabToCamelCase, KebabCase } from '../types'; + +export function isString(value: T): value is P { + return typeof value === 'string' || value instanceof String; +} + +export function snakeToCamel(str: string): string { + if (!str.match(/[\s_-]/g)) { + return str; + } + return str.replace(/([-_][a-z])/g, (group) => + group.toUpperCase().replace('-', '').replace('_', '') + ); +} + +export function camelToKebab(string: S): KebabCase { + return string + .replace(/((?<=[a-z\d])[A-Z]|(?<=[A-Z\d])[A-Z](?=[a-z]))/g, '-$1') + .toLowerCase() as KebabCase; +} + +export function upperFirstLetter(string: S): Capitalize { + return (string.charAt(0).toUpperCase() + string.slice(1)) as Capitalize; +} + +export function kebabToCamel(str: S): KebabToCamelCase { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join('') as KebabToCamelCase; +} + +export function capitalizeFirstChar(str: string) { + return str + .split('-') + .map((i) => i.charAt(0).toUpperCase() + i.substring(1)) + .join(''); +} From cbd1e71b1e89648c45d14e2708a0af33e14b0fa4 Mon Sep 17 00:00:00 2001 From: Alex H Date: Tue, 11 Feb 2025 07:39:15 +0100 Subject: [PATCH 22/26] refactor(json-api-nestjs-sdk): move to project use as separete package in next version --- .../src/lib/service/json-api-utils.service.ts | 2 +- libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts | 2 +- .../json-api-nestjs-sdk/src/lib/types/filter-operand.ts | 2 +- .../json-api-nestjs-sdk/src/lib/types/query-params.ts | 2 +- .../json-api-nestjs-sdk/src/lib/types/response-body.ts | 2 +- libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts | 2 +- libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts index 93fe473d..6f029dce 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/service/json-api-utils.service.ts @@ -1,4 +1,4 @@ -import { createEntityInstance } from '@klerick/json-api-nestjs-shared'; +import { createEntityInstance } from '../../shared'; import { Attributes, diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts index 6a2d03de..682c399a 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/entity.ts @@ -1,4 +1,4 @@ -import { EntityField, EntityProps } from '@klerick/json-api-nestjs-shared'; +import { EntityField, EntityProps } from '../../shared'; export { EntityField, EntityProps }; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts index 6e161821..c1f1a670 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/filter-operand.ts @@ -1,4 +1,4 @@ -import { FilterOperand } from '@klerick/json-api-nestjs-shared'; +import { FilterOperand } from '../../shared'; export { FilterOperand }; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts index c88c995a..8a555ac8 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/query-params.ts @@ -1,4 +1,4 @@ -import { QueryField } from '@klerick/json-api-nestjs-shared'; +import { QueryField } from '../../shared'; import { EntityProps, EntityRelation } from './entity'; import { TypeOfArray } from './utils'; import { Operands, OperandsRelation } from './filter-operand'; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts index fbd6a440..24813506 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/response-body.ts @@ -10,4 +10,4 @@ export { ResourceData, ResourceObject, ResourceObjectRelationships, -} from '@klerick/json-api-nestjs-shared'; +} from '../../shared'; diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts index e6a8a2a4..233d219b 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/types/utils.ts @@ -1,4 +1,4 @@ -import { TypeOfArray } from '@klerick/json-api-nestjs-shared'; +import { TypeOfArray } from '../../shared'; export { TypeOfArray }; type IntersectionToObj = { diff --git a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts index 2436ccc3..495bd3c5 100644 --- a/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts +++ b/libs/json-api/json-api-nestjs-sdk/src/lib/utils/index.ts @@ -1,4 +1,4 @@ -import { camelToKebab } from '@klerick/json-api-nestjs-shared'; +import { camelToKebab } from '../../shared'; import { JsonApiSdkConfig, JsonSdkConfig } from '../types'; import { ID_KEY } from '../constants'; @@ -14,7 +14,7 @@ export { capitalizeFirstChar, kebabToCamel, isObject, -} from '@klerick/json-api-nestjs-shared'; +} from '../../shared'; export function resultConfig(partialConfig: JsonSdkConfig): JsonApiSdkConfig { return { From bb8a4156bc1f5f64e380f1369006ef4878ca130c Mon Sep 17 00:00:00 2001 From: Alex H Date: Tue, 11 Feb 2025 07:40:08 +0100 Subject: [PATCH 23/26] refactor(json-api-nestjs): change npm package should be in next version --- libs/json-api/json-api-nestjs/README.md | 30 +++++++++++++++++---- libs/json-api/json-api-nestjs/package.json | 18 +++++++++++-- libs/json-api/json-api-nestjs/project.json | 31 +++++++++++++++++++++- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/libs/json-api/json-api-nestjs/README.md b/libs/json-api/json-api-nestjs/README.md index cba8c946..6f1db3a4 100644 --- a/libs/json-api/json-api-nestjs/README.md +++ b/libs/json-api/json-api-nestjs/README.md @@ -8,7 +8,7 @@ # json-api-nestjs -This plugin works upon TypeOrm library, which is used as the main database abstraction layer tool. The module +This plugin works upon TypeOrm or MicroOrm library, which is used as the main database abstraction layer tool. The module automatically generates an API according to JSON API specification from the database structure (TypeORM entities). It supports features such as requests validation based on database fields types, request filtering, endpoints extending, data relations control and much more. Our module significantly reduces the development time of REST services by removing @@ -24,15 +24,32 @@ $ npm install json-api-nestjs ## Example Once the installation process is complete, we can import the **JsonApiModule** into the root **AppModule**. +### TypeOrm +```typescript +import {Module} from '@nestjs/common'; +import {JsonApiModule, TypeOrmJsonApiModule} from 'json-api-nestjs'; +import {Users} from 'type-orm/database'; +@Module({ + imports: [ + JsonApiModule.forRoot(TypeOrmJsonApiModule, { + entities: [Users] + }), + ], +}) +export class AppModule { +} +``` + +### MicroOrm ```typescript import {Module} from '@nestjs/common'; -import {JsonApiModule} from 'json-api-nestjs'; -import {Users} from 'database'; +import {JsonApiModule, MicroOrmJsonApiModule} from 'json-api-nestjs'; +import {Users} from 'micro-orm/database'; @Module({ imports: [ - JsonApiModule.forRoot({ + JsonApiModule.forRoot(MicroOrmJsonApiModule, { entities: [Users] }), ], @@ -69,11 +86,14 @@ export interface ModuleOptions { debug?: boolean; // Debug info in result object, like error message pipeForId?: Type // Nestjs pipe for validate id params, by default ParseIntPipe operationUrl?: string // Url for atomic operation https://jsonapi.org/ext/atomic/ + // TypeOrm useSoftDelete?: boolean // Use soft delete runInTransaction?: any>( isolationLevel: IsolationLevel, fn: Func ) => ReturnType // You can use cutom function for wrapping transaction in atomic operation, example: runInTransaction from https://github.com/Aliheym/typeorm-transactional + // MicroOrm + arrayType?: string[]; //Custom type for indicate of array }; } ``` @@ -254,7 +274,7 @@ Available query params: ```typescript type FilterOperand { -in:string[] // is equal to the conditional of query "WHERE 'attribute_name' IN ('value1', 'value2')" + in:string[] // is equal to the conditional of query "WHERE 'attribute_name' IN ('value1', 'value2')" nin: string[] // is equal to the conditional of query "WHERE 'attribute_name' NOT IN ('value1', 'value1')" eq: string // is equal to the conditional of query "WHERE 'attribute_name' = 'value1' ne: string // is equal to the conditional of query "WHERE 'attribute_name' <> 'value1' diff --git a/libs/json-api/json-api-nestjs/package.json b/libs/json-api/json-api-nestjs/package.json index fe4641f5..b2231f27 100644 --- a/libs/json-api/json-api-nestjs/package.json +++ b/libs/json-api/json-api-nestjs/package.json @@ -1,5 +1,5 @@ { - "name": "@klerick/json-api-nestjs", + "name": "json-api-nestjs", "version": "8.0.0", "engines": { "node": ">= 16.0.0" @@ -27,10 +27,24 @@ "jsonapi", "json-api", "typeorm", + "microorm", "CRUD" ], "peerDependencies": { "reflect-metadata": "^0.1.13", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "@mikro-orm/core": "^6.0.0 || ^6.0.0-dev.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/swagger": "^7.3.0", + "@nestjs/typeorm": "^10.0.0", + "@mikro-orm/knex": "^6.0.0", + "typeorm": "^0.3.20" + }, + "dependencies": { + "@anatine/zod-openapi": "^2.0.0", + "zod": "^3.24.0", + "zod-validation-error": "^3.4.0", + "uuid": "^10.0.0" } } diff --git a/libs/json-api/json-api-nestjs/project.json b/libs/json-api/json-api-nestjs/project.json index be561184..4c6569a3 100644 --- a/libs/json-api/json-api-nestjs/project.json +++ b/libs/json-api/json-api-nestjs/project.json @@ -21,13 +21,42 @@ "tsConfig": "libs/json-api/json-api-nestjs/tsconfig.lib.json", "packageJson": "libs/json-api/json-api-nestjs/package.json", "main": "libs/json-api/json-api-nestjs/src/index.ts", - "assets": ["libs/json-api/json-api-nestjs/*.md"] + "assets": ["libs/json-api/json-api-nestjs/*.md"], + "buildableProjectDepsInPackageJsonType": "peerDependencies", + "generateExportsField": true } }, "nx-release-publish": { "options": { "packageRoot": "dist/{projectRoot}" } + }, + "publish": { + "command": "node tools/scripts/publish.mjs json-api-nestjs {args.ver} {args.tag}", + "dependsOn": ["build"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/json-api/json-api-nestjs/jest.config.ts", + "codeCoverage": true, + "coverageReporters": ["json-summary"] + } + }, + "upload-badge": { + "executor": "nx:run-commands", + "dependsOn": [ + { + "target": "test" + } + ], + "options": { + "commands": ["node tools/scripts/upload-badge.mjs json-api-nestjs"], + "cwd": "./", + "parallel": false, + "outputPath": "{workspaceRoot}/libs/json-api/json-api-nestjs" + } } } } From 0b7b4cc0ab1b17be32e10a81d769a06568c3bd1d Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 12 Feb 2025 05:35:41 +0100 Subject: [PATCH 24/26] refactor(json-api-nestjs): create modules for each orm --- .../extend-book-list.controller.ts | 10 +++ .../extend-user/extend-user.controller.ts | 30 ++++---- .../micro-orm/resources-micro.module.ts | 29 +++++++ .../service/atomic.interceptor.ts | 0 .../service/controller.interceptor.ts | 0 .../micro-orm/service/example.pipe.ts | 22 ++++++ .../service/example.service.ts | 0 .../{ => micro-orm}/service/guard.service.ts | 0 .../service/http-exception.filter.ts | 0 .../service/method.interceptor.ts | 0 .../extend-book-list.controller.ts | 8 +- .../extend-user/extend-user.controller.ts | 77 +++++++++++++++++++ .../type-orm/resources-type.module.ts | 29 +++++++ .../type-orm/service/atomic.interceptor.ts | 18 +++++ .../service/controller.interceptor.ts | 39 ++++++++++ .../{ => type-orm}/service/example.pipe.ts | 0 .../type-orm/service/example.service.ts | 5 ++ .../type-orm/service/guard.service.ts | 41 ++++++++++ .../type-orm/service/http-exception.filter.ts | 38 +++++++++ .../type-orm/service/method.interceptor.ts | 40 ++++++++++ 20 files changed, 367 insertions(+), 19 deletions(-) create mode 100644 apps/json-api-server/src/app/resources/micro-orm/controllers/extend-book-list/extend-book-list.controller.ts rename apps/json-api-server/src/app/resources/{ => micro-orm}/controllers/extend-user/extend-user.controller.ts (72%) create mode 100644 apps/json-api-server/src/app/resources/micro-orm/resources-micro.module.ts rename apps/json-api-server/src/app/resources/{ => micro-orm}/service/atomic.interceptor.ts (100%) rename apps/json-api-server/src/app/resources/{ => micro-orm}/service/controller.interceptor.ts (100%) create mode 100644 apps/json-api-server/src/app/resources/micro-orm/service/example.pipe.ts rename apps/json-api-server/src/app/resources/{ => micro-orm}/service/example.service.ts (100%) rename apps/json-api-server/src/app/resources/{ => micro-orm}/service/guard.service.ts (100%) rename apps/json-api-server/src/app/resources/{ => micro-orm}/service/http-exception.filter.ts (100%) rename apps/json-api-server/src/app/resources/{ => micro-orm}/service/method.interceptor.ts (100%) rename apps/json-api-server/src/app/resources/{ => type-orm}/controllers/extend-book-list/extend-book-list.controller.ts (75%) create mode 100644 apps/json-api-server/src/app/resources/type-orm/controllers/extend-user/extend-user.controller.ts create mode 100644 apps/json-api-server/src/app/resources/type-orm/resources-type.module.ts create mode 100644 apps/json-api-server/src/app/resources/type-orm/service/atomic.interceptor.ts create mode 100644 apps/json-api-server/src/app/resources/type-orm/service/controller.interceptor.ts rename apps/json-api-server/src/app/resources/{ => type-orm}/service/example.pipe.ts (100%) create mode 100644 apps/json-api-server/src/app/resources/type-orm/service/example.service.ts create mode 100644 apps/json-api-server/src/app/resources/type-orm/service/guard.service.ts create mode 100644 apps/json-api-server/src/app/resources/type-orm/service/http-exception.filter.ts create mode 100644 apps/json-api-server/src/app/resources/type-orm/service/method.interceptor.ts diff --git a/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-book-list/extend-book-list.controller.ts b/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-book-list/extend-book-list.controller.ts new file mode 100644 index 00000000..ab49a56f --- /dev/null +++ b/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-book-list/extend-book-list.controller.ts @@ -0,0 +1,10 @@ +import { ParseUUIDPipe } from '@nestjs/common'; +import { JsonApi, JsonBaseController } from '@klerick/json-api-nestjs'; + +import { BookList } from '@nestjs-json-api/microorm-database'; +@JsonApi(BookList, { + pipeForId: ParseUUIDPipe, + overrideRoute: 'override-book-list', + allowMethod: ['getOne', 'postOne', 'deleteOne'], +}) +export class ExtendBookListController extends JsonBaseController {} diff --git a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts b/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-user/extend-user.controller.ts similarity index 72% rename from apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts rename to apps/json-api-server/src/app/resources/micro-orm/controllers/extend-user/extend-user.controller.ts index 6682ba36..c2ba33f5 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-user/extend-user.controller.ts +++ b/apps/json-api-server/src/app/resources/micro-orm/controllers/extend-user/extend-user.controller.ts @@ -14,6 +14,7 @@ import { InjectService, JsonApiService, Query as QueryType, + QueryOne, ResourceObject, EntityRelation, PatchRelationshipData, @@ -29,35 +30,36 @@ import { HttpExceptionMethodFilter, } from '../../service/http-exception.filter'; import { GuardService, EntityName } from '../../service/guard.service'; -import { Users } from '../entity-orm'; + +import { Users } from '@nestjs-json-api/microorm-database'; import { AtomicInterceptor } from '../../service/atomic.interceptor'; @UseGuards(GuardService) @UseFilters(new HttpExceptionFilter()) @UseInterceptors(ControllerInterceptor) -@JsonApi(Users as any) -export class ExtendUserController extends JsonBaseController { - @InjectService() public service: JsonApiService; +@JsonApi(Users) +export class ExtendUserController extends JsonBaseController { + @InjectService() public service: JsonApiService; @Inject(ExampleService) protected exampleService: ExampleService; - getOne( + override getOne( id: string | number, - query: QueryType - ): Promise> { + query: QueryOne + ): Promise> { + const t = query.fields?.target; + return super.getOne(id, query); } - patchRelationship>( + patchRelationship>( id: string | number, relName: Rel, input: PatchRelationshipData - ): Promise> { + ): Promise> { return super.patchRelationship(id, relName, input); } // @UseInterceptors(AtomicInterceptor) - postOne( - inputData: PostData - ): Promise> { + postOne(inputData: PostData): Promise> { return super.postOne(inputData); } @@ -65,8 +67,8 @@ export class ExtendUserController extends JsonBaseController { @UseFilters(HttpExceptionMethodFilter) @UseInterceptors(MethodInterceptor) getAll( - @Query(ExamplePipe) query: QueryType - ): Promise> { + @Query(ExamplePipe) query: QueryType + ): Promise> { return super.getAll(query); } diff --git a/apps/json-api-server/src/app/resources/micro-orm/resources-micro.module.ts b/apps/json-api-server/src/app/resources/micro-orm/resources-micro.module.ts new file mode 100644 index 00000000..88248808 --- /dev/null +++ b/apps/json-api-server/src/app/resources/micro-orm/resources-micro.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { JsonApiModule, MicroOrmJsonApiModule } from '@klerick/json-api-nestjs'; +import { + Users, + Addresses, + Comments, + Roles, + BookList, +} from '@nestjs-json-api/microorm-database'; + +import { ExtendBookListController } from './controllers/extend-book-list/extend-book-list.controller'; +import { ExtendUserController } from './controllers/extend-user/extend-user.controller'; +import { ExampleService } from './service/example.service'; + +@Module({ + imports: [ + JsonApiModule.forRoot(MicroOrmJsonApiModule, { + entities: [Users, Addresses, Comments, Roles, BookList], + controllers: [ExtendBookListController, ExtendUserController], + providers: [ExampleService], + options: { + debug: true, + requiredSelectField: false, + operationUrl: 'operation', + }, + }), + ], +}) +export class ResourcesMicroModule {} diff --git a/apps/json-api-server/src/app/resources/service/atomic.interceptor.ts b/apps/json-api-server/src/app/resources/micro-orm/service/atomic.interceptor.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/atomic.interceptor.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/atomic.interceptor.ts diff --git a/apps/json-api-server/src/app/resources/service/controller.interceptor.ts b/apps/json-api-server/src/app/resources/micro-orm/service/controller.interceptor.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/controller.interceptor.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/controller.interceptor.ts diff --git a/apps/json-api-server/src/app/resources/micro-orm/service/example.pipe.ts b/apps/json-api-server/src/app/resources/micro-orm/service/example.pipe.ts new file mode 100644 index 00000000..a7b49347 --- /dev/null +++ b/apps/json-api-server/src/app/resources/micro-orm/service/example.pipe.ts @@ -0,0 +1,22 @@ +import { + ArgumentMetadata, + BadRequestException, + PipeTransform, +} from '@nestjs/common'; + +import { Query } from '@klerick/json-api-nestjs'; +import { Users } from '@nestjs-json-api/microorm-database'; + +export class ExamplePipe implements PipeTransform, Query> { + transform(value: Query, metadata: ArgumentMetadata): Query { + if (value.filter.target?.firstName?.eq === 'testCustomPipe') { + const error = { + code: 'invalid_arguments', + message: `Custom query pipe error`, + path: [], + }; + throw new BadRequestException([error]); + } + return value; + } +} diff --git a/apps/json-api-server/src/app/resources/service/example.service.ts b/apps/json-api-server/src/app/resources/micro-orm/service/example.service.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/example.service.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/example.service.ts diff --git a/apps/json-api-server/src/app/resources/service/guard.service.ts b/apps/json-api-server/src/app/resources/micro-orm/service/guard.service.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/guard.service.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/guard.service.ts diff --git a/apps/json-api-server/src/app/resources/service/http-exception.filter.ts b/apps/json-api-server/src/app/resources/micro-orm/service/http-exception.filter.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/http-exception.filter.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/http-exception.filter.ts diff --git a/apps/json-api-server/src/app/resources/service/method.interceptor.ts b/apps/json-api-server/src/app/resources/micro-orm/service/method.interceptor.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/method.interceptor.ts rename to apps/json-api-server/src/app/resources/micro-orm/service/method.interceptor.ts diff --git a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts b/apps/json-api-server/src/app/resources/type-orm/controllers/extend-book-list/extend-book-list.controller.ts similarity index 75% rename from apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts rename to apps/json-api-server/src/app/resources/type-orm/controllers/extend-book-list/extend-book-list.controller.ts index 248f78b0..170dab5b 100644 --- a/apps/json-api-server/src/app/resources/controllers/extend-book-list/extend-book-list.controller.ts +++ b/apps/json-api-server/src/app/resources/type-orm/controllers/extend-book-list/extend-book-list.controller.ts @@ -1,12 +1,10 @@ import { ParseUUIDPipe } from '@nestjs/common'; -import { BookList } from '../entity-orm'; import { JsonApi, JsonBaseController } from '@klerick/json-api-nestjs'; -@JsonApi(BookList as any, { +import { BookList } from '@nestjs-json-api/typeorm-database'; +@JsonApi(BookList, { pipeForId: ParseUUIDPipe, overrideRoute: 'override-book-list', allowMethod: ['getOne', 'postOne', 'deleteOne'], }) -export class ExtendBookListController extends JsonBaseController< - typeof BookList -> {} +export class ExtendBookListController extends JsonBaseController {} diff --git a/apps/json-api-server/src/app/resources/type-orm/controllers/extend-user/extend-user.controller.ts b/apps/json-api-server/src/app/resources/type-orm/controllers/extend-user/extend-user.controller.ts new file mode 100644 index 00000000..87b4f842 --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/controllers/extend-user/extend-user.controller.ts @@ -0,0 +1,77 @@ +import { + Get, + Param, + Inject, + Query, + UseInterceptors, + UseFilters, + UseGuards, +} from '@nestjs/common'; + +import { + JsonApi, + JsonBaseController, + InjectService, + JsonApiService, + Query as QueryType, + QueryOne, + ResourceObject, + EntityRelation, + PatchRelationshipData, + ResourceObjectRelationships, + PostData, +} from '@klerick/json-api-nestjs'; +import { ExamplePipe } from '../../service/example.pipe'; +import { ExampleService } from '../../service/example.service'; +import { ControllerInterceptor } from '../../service/controller.interceptor'; +import { MethodInterceptor } from '../../service/method.interceptor'; +import { + HttpExceptionFilter, + HttpExceptionMethodFilter, +} from '../../service/http-exception.filter'; +import { GuardService, EntityName } from '../../service/guard.service'; + +import { Users } from '@nestjs-json-api/typeorm-database'; +import { AtomicInterceptor } from '../../service/atomic.interceptor'; + +@UseGuards(GuardService) +@UseFilters(new HttpExceptionFilter()) +@UseInterceptors(ControllerInterceptor) +@JsonApi(Users) +export class ExtendUserController extends JsonBaseController { + @InjectService() public service: JsonApiService; + @Inject(ExampleService) protected exampleService: ExampleService; + override getOne( + id: string | number, + query: QueryOne + ): Promise> { + return super.getOne(id, query); + } + + patchRelationship>( + id: string | number, + relName: Rel, + input: PatchRelationshipData + ): Promise> { + return super.patchRelationship(id, relName, input); + } + + // @UseInterceptors(AtomicInterceptor) + postOne(inputData: PostData): Promise> { + return super.postOne(inputData); + } + + @EntityName('Users') + @UseFilters(HttpExceptionMethodFilter) + @UseInterceptors(MethodInterceptor) + getAll( + @Query(ExamplePipe) query: QueryType + ): Promise> { + return super.getAll(query); + } + + @Get(':id/example') + testOne(@Param('id') id: string): string { + return this.exampleService.testMethode(id); + } +} diff --git a/apps/json-api-server/src/app/resources/type-orm/resources-type.module.ts b/apps/json-api-server/src/app/resources/type-orm/resources-type.module.ts new file mode 100644 index 00000000..397eb04b --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/resources-type.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { JsonApiModule, TypeOrmJsonApiModule } from '@klerick/json-api-nestjs'; +import { + Users, + Addresses, + Comments, + Roles, + BookList, +} from '@nestjs-json-api/typeorm-database'; + +import { ExtendBookListController } from './controllers/extend-book-list/extend-book-list.controller'; +import { ExtendUserController } from './controllers/extend-user/extend-user.controller'; +import { ExampleService } from './service/example.service'; + +@Module({ + imports: [ + JsonApiModule.forRoot(TypeOrmJsonApiModule, { + entities: [Users, Addresses, Comments, Roles, BookList], + controllers: [ExtendBookListController, ExtendUserController], + providers: [ExampleService], + options: { + debug: true, + requiredSelectField: false, + operationUrl: 'operation', + }, + }), + ], +}) +export class ResourcesTypeModule {} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/atomic.interceptor.ts b/apps/json-api-server/src/app/resources/type-orm/service/atomic.interceptor.ts new file mode 100644 index 00000000..a52366ac --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/atomic.interceptor.ts @@ -0,0 +1,18 @@ +import { + CallHandler, + ExecutionContext, + NestInterceptor, + Injectable, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; + +@Injectable() +export class AtomicInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const isAtomic = context.getArgByIndex(3); + if (isAtomic) { + console.log('call from atomic operation'); + } + return next.handle(); + } +} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/controller.interceptor.ts b/apps/json-api-server/src/app/resources/type-orm/service/controller.interceptor.ts new file mode 100644 index 00000000..ee6db5ea --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/controller.interceptor.ts @@ -0,0 +1,39 @@ +import { + CallHandler, + ExecutionContext, + NestInterceptor, + Injectable, + BadRequestException, +} from '@nestjs/common'; +import { Observable, of, switchMap, throwError } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Request } from 'express'; +import { ImATeapotException } from '@nestjs/common/exceptions/im-a-teapot.exception'; + +@Injectable() +export class ControllerInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const query = context.switchToHttp().getRequest().query; + const typeCall = (query as any)?.filter?.firstName?.eq; + + return of(typeCall).pipe( + switchMap((r) => { + if (r === 'testControllerFilter') { + return throwError(() => new ImATeapotException()); + } + return next.handle(); + }), + map((r) => { + if (typeCall === 'testControllerInterceptor') { + const error = { + code: 'invalid_arguments', + message: `testControllerInterceptor error`, + path: [], + }; + throw new BadRequestException([error]); + } + return r; + }) + ); + } +} diff --git a/apps/json-api-server/src/app/resources/service/example.pipe.ts b/apps/json-api-server/src/app/resources/type-orm/service/example.pipe.ts similarity index 100% rename from apps/json-api-server/src/app/resources/service/example.pipe.ts rename to apps/json-api-server/src/app/resources/type-orm/service/example.pipe.ts diff --git a/apps/json-api-server/src/app/resources/type-orm/service/example.service.ts b/apps/json-api-server/src/app/resources/type-orm/service/example.service.ts new file mode 100644 index 00000000..be8fc1e8 --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/example.service.ts @@ -0,0 +1,5 @@ +export class ExampleService { + testMethode(id: string): string { + return id; + } +} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/guard.service.ts b/apps/json-api-server/src/app/resources/type-orm/service/guard.service.ts new file mode 100644 index 00000000..a8567bc2 --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/guard.service.ts @@ -0,0 +1,41 @@ +import { + BadRequestException, + CanActivate, + ExecutionContext, + ForbiddenException, + Injectable, +} from '@nestjs/common'; +import { Observable } from 'rxjs'; +import { entityForClass } from '@klerick/json-api-nestjs'; +import { Request } from 'express'; +import { Reflector } from '@nestjs/core'; + +export const EntityName = Reflector.createDecorator(); + +@Injectable() +export class GuardService implements CanActivate { + constructor(private reflector: Reflector) {} + + canActivate( + context: ExecutionContext + ): boolean | Promise | Observable { + const query = context.switchToHttp().getRequest().query; + const typeCall = (query as any)?.filter?.firstName?.eq; + + if (typeCall === 'testControllerGuard') { + return false; + } + + if (typeCall === 'testMethodeGuard') { + const entityName = this.reflector.get(EntityName, context.getHandler()); + if (!entityName) throw new BadRequestException(); + + // @ts-ignore + if (entityForClass(context.getClass()).name === entityName) { + throw new ForbiddenException('Not allow to ' + entityName); + } + } + + return true; + } +} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/http-exception.filter.ts b/apps/json-api-server/src/app/resources/type-orm/service/http-exception.filter.ts new file mode 100644 index 00000000..125a3db5 --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/http-exception.filter.ts @@ -0,0 +1,38 @@ +import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common'; +import { ImATeapotException } from '@nestjs/common/exceptions/im-a-teapot.exception'; +import { Request, Response } from 'express'; +import { PreconditionFailedException } from '@nestjs/common/exceptions/precondition-failed.exception'; + +@Catch(ImATeapotException) +export class HttpExceptionFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost): any { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = exception.getStatus(); + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + method: false, + }); + } +} + +@Catch(PreconditionFailedException) +export class HttpExceptionMethodFilter implements ExceptionFilter { + catch(exception: any, host: ArgumentsHost): any { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + const status = exception.getStatus(); + + response.status(status).json({ + statusCode: status, + timestamp: new Date().toISOString(), + path: request.url, + method: true, + }); + } +} diff --git a/apps/json-api-server/src/app/resources/type-orm/service/method.interceptor.ts b/apps/json-api-server/src/app/resources/type-orm/service/method.interceptor.ts new file mode 100644 index 00000000..37045f48 --- /dev/null +++ b/apps/json-api-server/src/app/resources/type-orm/service/method.interceptor.ts @@ -0,0 +1,40 @@ +import { + CallHandler, + ExecutionContext, + NestInterceptor, + Injectable, + BadRequestException, +} from '@nestjs/common'; +import { Observable, of, switchMap, throwError } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Request } from 'express'; +import { ImATeapotException } from '@nestjs/common/exceptions/im-a-teapot.exception'; +import { PreconditionFailedException } from '@nestjs/common/exceptions/precondition-failed.exception'; + +@Injectable() +export class MethodInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const query = context.switchToHttp().getRequest().query; + const typeCall = (query as any)?.filter?.firstName?.eq; + + return of(typeCall).pipe( + switchMap((r) => { + if (r === 'testMethodFilter') { + return throwError(() => new PreconditionFailedException()); + } + return next.handle(); + }), + map((r) => { + if (typeCall === 'testMethodInterceptor') { + const error = { + code: 'invalid_arguments', + message: `testMethodInterceptor error`, + path: [], + }; + throw new BadRequestException([error]); + } + return r; + }) + ); + } +} From e492cd186c970f1ee89615a71fdd0b90b5bdad4d Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 12 Feb 2025 05:38:21 +0100 Subject: [PATCH 25/26] fix(json-api-nestjs): Fix circular type for query obkect --- apps/json-api-server/src/app/app.module.ts | 10 +++- .../app/resources/controllers/entity-orm.ts | 10 ---- .../src/app/resources/resources.module.ts | 56 ------------------- libs/json-api/json-api-nestjs/project.json | 41 ++++++++++++-- libs/json-api/json-api-nestjs/src/index.ts | 1 + .../src/lib/mock-utils/microrom/index.ts | 1 - .../mixin/zod/zod-query-schema/fields.ts | 8 ++- .../mixin/zod/zod-query-schema/filter.ts | 8 ++- .../mixin/zod/zod-query-schema/index.ts | 2 +- 9 files changed, 55 insertions(+), 82 deletions(-) delete mode 100644 apps/json-api-server/src/app/resources/controllers/entity-orm.ts delete mode 100644 apps/json-api-server/src/app/resources/resources.module.ts diff --git a/apps/json-api-server/src/app/app.module.ts b/apps/json-api-server/src/app/app.module.ts index 9c39043a..28b47579 100644 --- a/apps/json-api-server/src/app/app.module.ts +++ b/apps/json-api-server/src/app/app.module.ts @@ -3,7 +3,8 @@ import { LoggerModule } from 'nestjs-pino'; import { TypeOrmDatabaseModule } from '@nestjs-json-api/typeorm-database'; import { MicroOrmDatabaseModule } from '@nestjs-json-api/microorm-database'; -import { ResourcesModule } from './resources/resources.module'; +import { ResourcesTypeModule } from './resources/type-orm/resources-type.module'; +import { ResourcesMicroModule } from './resources/micro-orm/resources-micro.module'; import { RpcModule } from './rpc/rpc.module'; import * as process from 'process'; @@ -12,10 +13,15 @@ const ormModule = ? TypeOrmDatabaseModule : MicroOrmDatabaseModule; +const resourceModule = + process.env['ORM_TYPE'] === 'typeorm' + ? ResourcesTypeModule + : ResourcesMicroModule; + @Module({ imports: [ ormModule, - ResourcesModule, + resourceModule, RpcModule, LoggerModule.forRoot({ pinoHttp: { diff --git a/apps/json-api-server/src/app/resources/controllers/entity-orm.ts b/apps/json-api-server/src/app/resources/controllers/entity-orm.ts deleted file mode 100644 index a95b88e7..00000000 --- a/apps/json-api-server/src/app/resources/controllers/entity-orm.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Users as tUsers } from '@nestjs-json-api/typeorm-database'; -import { Users as mkUsers } from '@nestjs-json-api/microorm-database'; - -import { BookList as tBookList } from '@nestjs-json-api/typeorm-database'; -import { BookList as mkBookList } from '@nestjs-json-api/microorm-database'; - -const Users = process.env['ORM_TYPE'] === 'typeorm' ? tUsers : mkUsers; -const BookList = process.env['ORM_TYPE'] === 'typeorm' ? tBookList : mkBookList; - -export { Users, BookList }; diff --git a/apps/json-api-server/src/app/resources/resources.module.ts b/apps/json-api-server/src/app/resources/resources.module.ts deleted file mode 100644 index 38cccd0b..00000000 --- a/apps/json-api-server/src/app/resources/resources.module.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Module } from '@nestjs/common'; -import { - JsonApiModule, - MicroOrmJsonApiModule, - TypeOrmJsonApiModule, -} from '@klerick/json-api-nestjs'; -import { - Users, - Addresses, - Comments, - Roles, - BookList, -} from '@nestjs-json-api/typeorm-database'; - -import { - Users as mkUsers, - Addresses as mkAddresses, - Comments as mkComments, - Roles as mkRoles, - BookList as mkBookList, -} from '@nestjs-json-api/microorm-database'; - -import { ExtendBookListController } from './controllers/extend-book-list/extend-book-list.controller'; -import { ExtendUserController } from './controllers/extend-user/extend-user.controller'; -import { ExampleService } from './service/example.service'; - -const typeOrm = () => - JsonApiModule.forRoot(TypeOrmJsonApiModule, { - entities: [Users, Addresses, Comments, Roles, BookList], - controllers: [ExtendBookListController, ExtendUserController], - providers: [ExampleService], - options: { - debug: true, - requiredSelectField: false, - operationUrl: 'operation', - }, - }); - -const microOrm = () => - JsonApiModule.forRoot(MicroOrmJsonApiModule, { - entities: [mkUsers, mkAddresses, mkComments, mkRoles, mkBookList], - controllers: [ExtendBookListController, ExtendUserController], - providers: [ExampleService], - options: { - debug: true, - requiredSelectField: false, - operationUrl: 'operation', - }, - }); - -const ormModule = process.env['ORM_TYPE'] === 'typeorm' ? typeOrm : microOrm; - -@Module({ - imports: [ormModule()], -}) -export class ResourcesModule {} diff --git a/libs/json-api/json-api-nestjs/project.json b/libs/json-api/json-api-nestjs/project.json index 4c6569a3..ad65b72e 100644 --- a/libs/json-api/json-api-nestjs/project.json +++ b/libs/json-api/json-api-nestjs/project.json @@ -15,13 +15,34 @@ "targets": { "build": { "executor": "@nx/js:tsc", - "outputs": ["{options.outputPath}"], + "outputs": [ + "{options.outputPath}" + ], "options": { "outputPath": "dist/libs/json-api/json-api-nestjs", "tsConfig": "libs/json-api/json-api-nestjs/tsconfig.lib.json", "packageJson": "libs/json-api/json-api-nestjs/package.json", "main": "libs/json-api/json-api-nestjs/src/index.ts", - "assets": ["libs/json-api/json-api-nestjs/*.md"], + "assets": [ + "libs/json-api/json-api-nestjs/*.md" + ], + "buildableProjectDepsInPackageJsonType": "peerDependencies", + "generateExportsField": true + } + }, + "build-npm": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "node_modules/@klerick/json-api-nestjs", + "tsConfig": "libs/json-api/json-api-nestjs/tsconfig.lib.json", + "packageJson": "libs/json-api/json-api-nestjs/package.json", + "main": "libs/json-api/json-api-nestjs/src/index.ts", + "assets": [ + "libs/json-api/json-api-nestjs/*.md" + ], "buildableProjectDepsInPackageJsonType": "peerDependencies", "generateExportsField": true } @@ -33,15 +54,21 @@ }, "publish": { "command": "node tools/scripts/publish.mjs json-api-nestjs {args.ver} {args.tag}", - "dependsOn": ["build"] + "dependsOn": [ + "build" + ] }, "test": { "executor": "@nx/jest:jest", - "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}" + ], "options": { "jestConfig": "libs/json-api/json-api-nestjs/jest.config.ts", "codeCoverage": true, - "coverageReporters": ["json-summary"] + "coverageReporters": [ + "json-summary" + ] } }, "upload-badge": { @@ -52,7 +79,9 @@ } ], "options": { - "commands": ["node tools/scripts/upload-badge.mjs json-api-nestjs"], + "commands": [ + "node tools/scripts/upload-badge.mjs json-api-nestjs" + ], "cwd": "./", "parallel": false, "outputPath": "{workspaceRoot}/libs/json-api/json-api-nestjs" diff --git a/libs/json-api/json-api-nestjs/src/index.ts b/libs/json-api/json-api-nestjs/src/index.ts index 50c6ef0e..fc0b6e76 100644 --- a/libs/json-api/json-api-nestjs/src/index.ts +++ b/libs/json-api/json-api-nestjs/src/index.ts @@ -10,6 +10,7 @@ export { PostData, PostRelationshipData, PatchRelationshipData, + QueryOne, } from './lib/modules/mixin/zod'; export { diff --git a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts index 9c542b6d..ed7ef0d9 100644 --- a/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/mock-utils/microrom/index.ts @@ -1,4 +1,3 @@ -import { DynamicModule } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { EntityManager, MikroORM } from '@mikro-orm/core'; diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts index 3d647940..5b8f65c8 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/fields.ts @@ -20,6 +20,10 @@ type ZodRule = ReturnType< typeof getZodRules >; +type TargetRelationShape = { + [K in keyof RelationTree]: ZodRule[K]>; +}; + export function zodFieldsQuery( fields: ResultGetField['field'], relationList: RelationTree @@ -28,9 +32,7 @@ export function zodFieldsQuery( target: getZodRules(fields), }; - const relation = {} as { - [K in keyof RelationTree]: ZodRule[K]>; - }; + const relation = {} as TargetRelationShape; for (const [key, value] of ObjectTyped.entries(relationList)) { relation[key] = getZodRules(value); diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts index 5e92b607..8253787c 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/filter.ts @@ -163,6 +163,10 @@ type RelationFilterProps = { [R in keyof RelationTree]: ZodRulesForRelationShape>; }; +type TargetRelationShape = { + [K in ResultGetField['relations'][number]]: ZodOptional; +}; + export function zodFilterQuery( fields: ResultGetField['field'], relationTree: RelationTree, @@ -182,9 +186,7 @@ export function zodFilterQuery( ...acum, [item]: zodRuleFilterRelationSchema.optional(), }), - {} as { - [K in ResultGetField['relations'][number]]: ZodOptional; - } + {} as TargetRelationShape ); const relationFilterProps = ObjectTyped.keys(relationTree).reduce( diff --git a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts index d984dded..9483f211 100644 --- a/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts +++ b/libs/json-api/json-api-nestjs/src/lib/modules/mixin/zod/zod-query-schema/index.ts @@ -81,7 +81,7 @@ function zodQueryOne( entityRelationStructure: RelationTree, propsArray: ArrayPropsForEntity, propsType: AllFieldWithType -) { +): ZodObject, QueryField.fields | QueryField.include>, 'strict'> { return z .object({ [QueryField.fields]: zodFieldsQuery( From c2e9d1d8fdc13b0170e23a36bf5e6976a14322eb Mon Sep 17 00:00:00 2001 From: Alex H Date: Wed, 12 Feb 2025 06:22:57 +0100 Subject: [PATCH 26/26] docs(json-api-nestjs): Add describe add micro orm in readme Closes: 97 --- README.md | 25 ++++++++++++++++--------- libs/json-api/json-api-nestjs/README.md | 4 ++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2893a528..b268dff2 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,24 @@

- Json API plugins for - NestJS - framework +NestJS JSON API & JSON RPC Suite

+

- Tools to implement JSON API, such as, end point, query params, body params, validation and transformation response. + This monorepo contains a set of several libraries designed to simplify the development of server and client applications using NestJS. These tools help you work with two popular protocols:

-- *[json-api-nestjs](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs)* - plugin for create CRUD overs JSON API -- *[json-api-nestjs-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs-sdk)* - tool for client, call api over *json-api-nestjs* -- *[nestjs-json-rpc](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc)* - plugin for create RPC server using [JSON-RPC](https://www.jsonrpc.org/) -- *[nestjs-json-rpc-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc-sdk)* - tool for client, call RPC server *nestjs-json-rpc* -- *json-api-nestjs-acl* - tool for acl over *json-api-nestjs*(coming soon...) + +- **[JSON:API](https://jsonapi.org/)** – A specification for building RESTful APIs with standardized request and response formats. + + > **[json-api-nestjs](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs)** - This package enables you to quickly set up a server API that adheres to the JSON:API specification, handling standard CRUD operations for your resources.
+ > **[json-api-nestjs-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-api/json-api-nestjs-sdk)** - tool for client, call api over *json-api-nestjs* + + +- **[JSON-RPC](https://www.jsonrpc.org/)** – A protocol for remote procedure calls using JSON. + +> **[nestjs-json-rpc](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc)** - Use this package to implement remote procedure call (RPC) functionality in your NestJS applications, enabling efficient inter-service communication.
+> **[nestjs-json-rpc-sdk](https://github.com/klerick/nestjs-json-api/tree/master/libs/json-rpc/nestjs-json-rpc-sdk)** - This tool offers a straightforward way to call remote procedures from your client-side code, ensuring smooth communication with your JSON-RPC server. + +- **ACL tools** - tool for acl over *json-api-nestjs*(coming soon...) ## Installation ```bash diff --git a/libs/json-api/json-api-nestjs/README.md b/libs/json-api/json-api-nestjs/README.md index 6f1db3a4..ffa4bf6a 100644 --- a/libs/json-api/json-api-nestjs/README.md +++ b/libs/json-api/json-api-nestjs/README.md @@ -8,8 +8,8 @@ # json-api-nestjs -This plugin works upon TypeOrm or MicroOrm library, which is used as the main database abstraction layer tool. The module -automatically generates an API according to JSON API specification from the database structure (TypeORM entities). It +This plugin works upon **TypeOrm** or **MicroOrm** library, which is used as the main database abstraction layer tool. The module +automatically generates an API according to JSON API specification from the database structure (**TypeOrm** or **MicroOrm** entities). It supports features such as requests validation based on database fields types, request filtering, endpoints extending, data relations control and much more. Our module significantly reduces the development time of REST services by removing the need to negotiate the mechanism of client-server interaction and implementing automatic API generation without the